From 24f006fe4abac545607384f56d736a699f0d32d3 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 16 Aug 2026 06:20:08 +0900 Subject: [PATCH] feat(cpp-object-oriented-modular-refactoring): step 5 - solver-workflow-google-style --- include/fesa/analysis/analysis_model.h | 55 + include/fesa/analysis/analysis_model.hpp | 34 - include/fesa/analysis/analysis_state.h | 102 + include/fesa/analysis/analysis_state.hpp | 66 - .../fesa/analysis/linear_static_analysis.h | 104 + .../fesa/analysis/linear_static_analysis.hpp | 78 - include/fesa/assembly/load_assembler.h | 29 + include/fesa/assembly/load_assembler.hpp | 24 - include/fesa/assembly/parallel_for.h | 39 + include/fesa/assembly/parallel_for.hpp | 32 - include/fesa/assembly/sparse_assembler.h | 27 + include/fesa/assembly/sparse_assembler.hpp | 20 - .../fesa/constraints/essential_constraints.h | 41 + .../constraints/essential_constraints.hpp | 35 - include/fesa/fem/dof_manager.h | 83 + include/fesa/fem/dof_manager.hpp | 69 - include/fesa/io/hdf5/hdf5_results_writer.hpp | 4 +- include/fesa/results/result_records.h | 86 + include/fesa/results/result_records.hpp | 83 - include/fesa/results/result_recovery.h | 43 + include/fesa/results/result_recovery.hpp | 36 - include/fesa/results/results_writer.h | 28 + include/fesa/results/results_writer.hpp | 25 - src/fesa/analysis/analysis_model.cpp | 130 +- src/fesa/analysis/analysis_state.cpp | 356 ++- src/fesa/analysis/linear_static_analysis.cpp | 278 ++- src/fesa/app/fesa_application.cpp | 10 +- src/fesa/assembly/load_assembler.cpp | 813 ++++--- src/fesa/assembly/parallel_for.cpp | 38 +- src/fesa/assembly/sparse_assembler.cpp | 577 +++-- .../constraints/essential_constraints.cpp | 411 ++-- src/fesa/fem/dof_manager.cpp | 427 ++-- src/fesa/io/hdf5/hdf5_results_writer.cpp | 158 +- src/fesa/math/sparse_matrix.cpp | 22 +- src/fesa/results/result_recovery.cpp | 1950 ++++++++--------- .../analysis/linear_static_analysis_test.cpp | 690 +++--- tests/reference/reference_comparison.cpp | 22 +- tests/reference/reference_comparison_test.cpp | 30 +- tests/unit/analysis/analysis_model_test.cpp | 260 +-- tests/unit/analysis/analysis_state_test.cpp | 237 +- tests/unit/assembly/load_assembler_test.cpp | 757 +++---- tests/unit/assembly/parallel_for_test.cpp | 156 +- tests/unit/assembly/sparse_assembler_test.cpp | 550 +++-- .../essential_constraints_test.cpp | 558 +++-- tests/unit/fem/dof_manager_test.cpp | 384 ++-- .../unit/io/hdf5/hdf5_results_writer_test.cpp | 96 +- tests/unit/math/sparse_matrix_test.cpp | 10 +- tests/unit/results/result_records_test.cpp | 500 ++--- tests/unit/results/result_recovery_test.cpp | 1580 ++++++------- tests/unit/results/results_writer_test.cpp | 23 +- .../solvers/linear/linear_solver_test.cpp | 10 +- .../linear/mkl_pardiso_solver_test.cpp | 10 +- 52 files changed, 5817 insertions(+), 6369 deletions(-) create mode 100644 include/fesa/analysis/analysis_model.h delete mode 100644 include/fesa/analysis/analysis_model.hpp create mode 100644 include/fesa/analysis/analysis_state.h delete mode 100644 include/fesa/analysis/analysis_state.hpp create mode 100644 include/fesa/analysis/linear_static_analysis.h delete mode 100644 include/fesa/analysis/linear_static_analysis.hpp create mode 100644 include/fesa/assembly/load_assembler.h delete mode 100644 include/fesa/assembly/load_assembler.hpp create mode 100644 include/fesa/assembly/parallel_for.h delete mode 100644 include/fesa/assembly/parallel_for.hpp create mode 100644 include/fesa/assembly/sparse_assembler.h delete mode 100644 include/fesa/assembly/sparse_assembler.hpp create mode 100644 include/fesa/constraints/essential_constraints.h delete mode 100644 include/fesa/constraints/essential_constraints.hpp create mode 100644 include/fesa/fem/dof_manager.h delete mode 100644 include/fesa/fem/dof_manager.hpp create mode 100644 include/fesa/results/result_records.h delete mode 100644 include/fesa/results/result_records.hpp create mode 100644 include/fesa/results/result_recovery.h delete mode 100644 include/fesa/results/result_recovery.hpp create mode 100644 include/fesa/results/results_writer.h delete mode 100644 include/fesa/results/results_writer.hpp diff --git a/include/fesa/analysis/analysis_model.h b/include/fesa/analysis/analysis_model.h new file mode 100644 index 0000000..83c001e --- /dev/null +++ b/include/fesa/analysis/analysis_model.h @@ -0,0 +1,55 @@ +#ifndef FESA_ANALYSIS_ANALYSIS_MODEL_H_ +#define FESA_ANALYSIS_ANALYSIS_MODEL_H_ + +#include + +#include "fesa/model/domain.h" + +namespace fesa { + +/// @brief Provides the active-step view into a non-owned Domain. +/// @note The referenced Domain must outlive this object and retains all +/// semantic ownership. +class AnalysisModel { + public: + /// @brief Creates the sole active-step view for a valid Domain. + /// @param domain Domain that remains alive for the returned view's lifetime. + /// @return A stable view or an input-cardinality failure. + static Result Create(const Domain& domain); + + /// @brief Returns the non-owned Domain backing this view. + const Domain& GetDomain() const noexcept; + + /// @brief Returns the sole active static step. + const StaticStepDefinition& Step() const noexcept; + + /// @brief Returns active beam element indices in stable internal order. + const std::vector& ActiveElements() const noexcept; + + /// @brief Returns reachable material indices in stable internal order. + const std::vector& ActiveMaterials() const noexcept; + + /// @brief Returns reachable beam-section indices in stable internal order. + const std::vector& ActiveSections() const noexcept; + + /// @brief Returns boundary-condition indices in source order. + const std::vector& ActiveBoundaryConditions() const noexcept; + + /// @brief Returns concentrated-load indices in source order. + const std::vector& ActiveLoads() const noexcept; + + private: + /// @brief Builds stable indices without copying the referenced Domain. + explicit AnalysisModel(const Domain& domain); + + const Domain* domain_; + std::vector active_elements_; + std::vector active_materials_; + std::vector active_sections_; + std::vector active_boundary_conditions_; + std::vector active_loads_; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_ANALYSIS_MODEL_H_ diff --git a/include/fesa/analysis/analysis_model.hpp b/include/fesa/analysis/analysis_model.hpp deleted file mode 100644 index b5e0f80..0000000 --- a/include/fesa/analysis/analysis_model.hpp +++ /dev/null @@ -1,34 +0,0 @@ -#pragma once - -#include "fesa/model/domain.h" - -#include - -namespace fesa { - -// Provides the sole active-step view while the referenced Domain retains all -// semantic ownership and must outlive this object. -class AnalysisModel { -public: - static Result create(const Domain& domain); - - const Domain& domain() const noexcept; - const StaticStepDefinition& step() const noexcept; - const std::vector& activeElements() const noexcept; - const std::vector& activeMaterials() const noexcept; - const std::vector& activeSections() const noexcept; - const std::vector& activeBoundaryConditions() const noexcept; - const std::vector& activeLoads() const noexcept; - -private: - explicit AnalysisModel(const Domain& domain); - - const Domain* domain_; - std::vector activeElements_; - std::vector activeMaterials_; - std::vector activeSections_; - std::vector activeBoundaryConditions_; - std::vector activeLoads_; -}; - -} // namespace fesa diff --git a/include/fesa/analysis/analysis_state.h b/include/fesa/analysis/analysis_state.h new file mode 100644 index 0000000..d6d4031 --- /dev/null +++ b/include/fesa/analysis/analysis_state.h @@ -0,0 +1,102 @@ +#ifndef FESA_ANALYSIS_ANALYSIS_STATE_H_ +#define FESA_ANALYSIS_ANALYSIS_STATE_H_ + +#include +#include +#include + +#include "fesa/core/status.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/math/vector.h" +#include "fesa/results/result_records.h" + +namespace fesa { + +/// @brief Owns mutable quantities required by the V0 linear-static frame. +class AnalysisState { + public: + /// @brief Allocates zeroed full-DOF vectors for a DOF manager. + /// @param dofs Owner of the full-DOF dimension used by every state vector. + /// @param identity Stable step and frame identity for this state. + static AnalysisState Create(const DofManager& dofs, + StepFrameIdentity identity); + + /// @brief Returns mutable full-space displacement. + Vector& Displacement() noexcept; + /// @brief Returns full-space displacement. + const Vector& Displacement() const noexcept; + /// @brief Returns mutable full-space external force. + Vector& ExternalForce() noexcept; + /// @brief Returns full-space external force. + const Vector& ExternalForce() const noexcept; + /// @brief Returns mutable full-space internal force. + Vector& InternalForce() noexcept; + /// @brief Returns full-space internal force. + const Vector& InternalForce() const noexcept; + /// @brief Returns mutable full residual K*d-F. + Vector& Residual() noexcept; + /// @brief Returns full residual K*d-F. + const Vector& Residual() const noexcept; + /// @brief Returns mutable full-index reaction and free residual evidence. + Vector& Reaction() noexcept; + /// @brief Returns full-index reaction and free residual evidence. + const Vector& Reaction() const noexcept; + /// @brief Returns the stable step and frame identity. + const StepFrameIdentity& Identity() const noexcept; + /// @brief Returns mutable beam endpoint result rows. + std::vector& EndpointResults() noexcept; + /// @brief Returns beam endpoint result rows. + const std::vector& EndpointResults() const noexcept; + /// @brief Returns mutable beam Gauss result rows. + std::vector& GaussResults() noexcept; + /// @brief Returns beam Gauss result rows. + const std::vector& GaussResults() const noexcept; + /// @brief Returns mutable beam axial-stress rows. + std::vector& StressResults() noexcept; + /// @brief Returns beam axial-stress rows. + const std::vector& StressResults() const noexcept; + + /// @brief Validates and atomically replaces all shell recovery evidence. + /// @param expected_element_order Unique shell indices in stable order. + /// @param candidate Complete shell rows, energy, and equilibrium evidence. + /// @return Success only after the complete candidate is validated and + /// committed; failure preserves the prior shell state. + Status CommitShellResults( + const std::vector& expected_element_order, + ShellStateCandidate candidate); + + /// @brief Returns shell rows in stable element and location order. + const std::vector& ShellResults() const noexcept; + /// @brief Returns physical shell strain energy without drilling energy. + double PhysicalStrainEnergy() const noexcept; + /// @brief Returns global force and moment equilibrium components. + const std::array& Equilibrium() const noexcept; + /// @brief Returns normalized shell verification metrics. + const std::array& VerificationMetrics() const noexcept; + + private: + /// @brief Allocates state storage for one stable full-DOF dimension. + AnalysisState(std::size_t full_dof_count, StepFrameIdentity identity); + + StepFrameIdentity identity_; + Vector displacement_; + Vector external_force_; + Vector internal_force_; + Vector residual_; + // Reactions retain full-index space so free residual components remain + // visible. + Vector reaction_; + // Recovery appends rows in stable element/location order. + std::vector endpoint_results_; + std::vector gauss_results_; + std::vector stress_results_; + // Shell recovery is replaced only through validated candidate commit. + std::vector shell_results_; + double physical_strain_energy_{0.0}; + std::array equilibrium_{}; + std::array verification_metrics_{}; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_ANALYSIS_STATE_H_ diff --git a/include/fesa/analysis/analysis_state.hpp b/include/fesa/analysis/analysis_state.hpp deleted file mode 100644 index 1dd1286..0000000 --- a/include/fesa/analysis/analysis_state.hpp +++ /dev/null @@ -1,66 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/math/vector.h" -#include "fesa/results/result_records.hpp" - -#include -#include -#include - -namespace fesa { - -// Owns only the mutable quantities required by the V0 linear-static frame. -class AnalysisState { -public: - static AnalysisState create( - const DofManager& dofs, StepFrameIdentity identity); - - Vector& displacement() noexcept; - const Vector& displacement() const noexcept; - Vector& externalForce() noexcept; - const Vector& externalForce() const noexcept; - Vector& internalForce() noexcept; - const Vector& internalForce() const noexcept; - Vector& residual() noexcept; - const Vector& residual() const noexcept; - Vector& reaction() noexcept; - const Vector& reaction() const noexcept; - const StepFrameIdentity& identity() const noexcept; - std::vector& endpointResults() noexcept; - const std::vector& endpointResults() const noexcept; - std::vector& gaussResults() noexcept; - const std::vector& gaussResults() const noexcept; - std::vector& stressResults() noexcept; - const std::vector& stressResults() const noexcept; - Status commitShellResults( - const std::vector& expectedElementOrder, - ShellStateCandidate candidate); - const std::vector& shellResults() const noexcept; - double physicalStrainEnergy() const noexcept; - const std::array& equilibrium() const noexcept; - const std::array& verificationMetrics() const noexcept; - -private: - AnalysisState(std::size_t fullDofCount, StepFrameIdentity identity); - - StepFrameIdentity identity_; - Vector displacement_; - Vector externalForce_; - Vector internalForce_; - Vector residual_; - // Reactions retain full-index space so free residual components remain visible. - Vector reaction_; - // Recovery appends rows in stable element/location order. - std::vector endpointResults_; - std::vector gaussResults_; - std::vector stressResults_; - // Shell recovery is replaced only through validated candidate commit. - std::vector shellResults_; - double physicalStrainEnergy_{0.0}; - std::array equilibrium_{}; - std::array verificationMetrics_{}; -}; - -} // namespace fesa diff --git a/include/fesa/analysis/linear_static_analysis.h b/include/fesa/analysis/linear_static_analysis.h new file mode 100644 index 0000000..c0c1fba --- /dev/null +++ b/include/fesa/analysis/linear_static_analysis.h @@ -0,0 +1,104 @@ +#ifndef FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_ +#define FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_ + +#include +#include +#include + +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/constraints/essential_constraints.h" +#include "fesa/core/status.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" +#include "fesa/model/domain.h" + +namespace fesa { + +class LinearSolver; +class ParallelFor; +class ResultsWriter; + +/// @brief Carries input and authoritative output paths for one analysis run. +struct AnalysisRequest { + std::filesystem::path input_path; + std::filesystem::path output_path; +}; + +/// @brief Executes the current approved V0 analysis lifecycle. +class Analysis { + public: + virtual ~Analysis() = default; + + /// @brief Runs the eight current lifecycle stages in their approved order. + /// @param request Input and output paths for this run. + /// @return The first stage failure or successful result finalization. + Status Run(const AnalysisRequest& request); + + protected: + /// @brief Initializes owned input and Domain state for a run. + virtual Status Initialize(const AnalysisRequest& request) = 0; + /// @brief Builds the non-owning active-model view. + virtual Status BuildAnalysisModel() = 0; + /// @brief Builds stable DOF numbering and the sparse structural pattern. + virtual Status BuildDofMapAndSparsePattern() = 0; + /// @brief Assembles full stiffness and stable constraint partitions. + virtual Status AssembleAndPartitionStiffness() = 0; + /// @brief Factorizes Kff before any load assembly. + virtual Status Factorize() = 0; + /// @brief Assembles loads and forms Ff-Kfc*dc without solving. + virtual Status AssembleLoadsAndEffectiveRhs() = 0; + /// @brief Substitutes the retained factorization and reconstructs full d. + virtual Status SubstituteAndReconstruct() = 0; + /// @brief Recovers a complete candidate and writes authoritative results. + virtual Status RecoverAndWriteResults() = 0; +}; + +/// @brief Orchestrates the single-step linear-static procedure. +/// @note Factorization, substitution, recovery, and writing remain separately +/// observable through injected backend boundaries. +class LinearStaticAnalysis final : public Analysis { + public: + /// @brief Creates a procedure using non-owned backend adapters. + /// @note All three adapters must outlive this analysis object. + LinearStaticAnalysis(const ParallelFor& parallel_for, + LinearSolver& linear_solver, + ResultsWriter& results_writer); + + protected: + /// @copydoc Analysis::Initialize + Status Initialize(const AnalysisRequest& request) override; + /// @copydoc Analysis::BuildAnalysisModel + Status BuildAnalysisModel() override; + /// @copydoc Analysis::BuildDofMapAndSparsePattern + Status BuildDofMapAndSparsePattern() override; + /// @copydoc Analysis::AssembleAndPartitionStiffness + Status AssembleAndPartitionStiffness() override; + /// @copydoc Analysis::Factorize + Status Factorize() override; + /// @copydoc Analysis::AssembleLoadsAndEffectiveRhs + Status AssembleLoadsAndEffectiveRhs() override; + /// @copydoc Analysis::SubstituteAndReconstruct + Status SubstituteAndReconstruct() override; + /// @copydoc Analysis::RecoverAndWriteResults + Status RecoverAndWriteResults() override; + + private: + const ParallelFor& parallel_for_; + LinearSolver& linear_solver_; + ResultsWriter& results_writer_; + AnalysisRequest request_; + std::unique_ptr domain_; + std::unique_ptr model_; + std::unique_ptr dofs_; + std::unique_ptr state_; + std::unique_ptr full_stiffness_; + std::unique_ptr partitioned_stiffness_; + std::unique_ptr effective_rhs_; + std::vector diagnostics_; +}; + +} // namespace fesa + +#endif // FESA_ANALYSIS_LINEAR_STATIC_ANALYSIS_H_ diff --git a/include/fesa/analysis/linear_static_analysis.hpp b/include/fesa/analysis/linear_static_analysis.hpp deleted file mode 100644 index 8b33014..0000000 --- a/include/fesa/analysis/linear_static_analysis.hpp +++ /dev/null @@ -1,78 +0,0 @@ -#pragma once - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/constraints/essential_constraints.hpp" -#include "fesa/core/status.h" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/math/sparse_matrix.h" -#include "fesa/math/vector.h" -#include "fesa/model/domain.h" - -#include -#include -#include - -namespace fesa { - -class LinearSolver; -class ParallelFor; -class ResultsWriter; - -struct AnalysisRequest { - std::filesystem::path inputPath; - std::filesystem::path outputPath; -}; - -// Fixes the public V0 lifecycle while leaving each analysis procedure to -// implement its approved stages. -class Analysis { -public: - virtual ~Analysis() = default; - Status run(const AnalysisRequest& request); - -protected: - virtual Status initialize(const AnalysisRequest& request) = 0; - virtual Status buildAnalysisModel() = 0; - virtual Status buildDofMapAndSparsePattern() = 0; - virtual Status assembleAndPartitionStiffness() = 0; - virtual Status factorize() = 0; - virtual Status assembleLoadsAndEffectiveRhs() = 0; - virtual Status substituteAndReconstruct() = 0; - virtual Status recoverAndWriteResults() = 0; -}; - -// Orchestrates the single-step B33 procedure through injected backend -// boundaries so factorization and substitution remain independently visible. -class LinearStaticAnalysis final : public Analysis { -public: - LinearStaticAnalysis(const ParallelFor& parallelFor, - LinearSolver& linearSolver, - ResultsWriter& resultsWriter); - -protected: - Status initialize(const AnalysisRequest& request) override; - Status buildAnalysisModel() override; - Status buildDofMapAndSparsePattern() override; - Status assembleAndPartitionStiffness() override; - Status factorize() override; - Status assembleLoadsAndEffectiveRhs() override; - Status substituteAndReconstruct() override; - Status recoverAndWriteResults() override; - -private: - const ParallelFor& parallelFor_; - LinearSolver& linearSolver_; - ResultsWriter& resultsWriter_; - AnalysisRequest request_; - std::unique_ptr domain_; - std::unique_ptr model_; - std::unique_ptr dofs_; - std::unique_ptr state_; - std::unique_ptr fullStiffness_; - std::unique_ptr partitionedStiffness_; - std::unique_ptr effectiveRhs_; - std::vector diagnostics_; -}; - -} // namespace fesa diff --git a/include/fesa/assembly/load_assembler.h b/include/fesa/assembly/load_assembler.h new file mode 100644 index 0000000..6ea8d3e --- /dev/null +++ b/include/fesa/assembly/load_assembler.h @@ -0,0 +1,29 @@ +#ifndef FESA_ASSEMBLY_LOAD_ASSEMBLER_H_ +#define FESA_ASSEMBLY_LOAD_ASSEMBLER_H_ + +#include "fesa/analysis/analysis_model.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" + +namespace fesa { + +/// @brief Assembles semantic nodal loads in stable source order. +class LoadAssembler { + public: + /// @brief Accumulates all active CLOAD rows in full-DOF space. + /// @return A finite full load vector or a structured model failure. + static Result AssembleFullNodalLoad(const AnalysisModel& model, + const DofManager& dofs); + + /// @brief Forms Ff-Kfc*dc in stable free/constrained order. + /// @note This operation neither factorizes nor invokes a solver. + static Result EffectiveFreeRhs(const Vector& full_load, + const SparseMatrix& kfc, + const Vector& prescribed_values, + const DofManager& dofs); +}; + +} // namespace fesa + +#endif // FESA_ASSEMBLY_LOAD_ASSEMBLER_H_ diff --git a/include/fesa/assembly/load_assembler.hpp b/include/fesa/assembly/load_assembler.hpp deleted file mode 100644 index a09afda..0000000 --- a/include/fesa/assembly/load_assembler.hpp +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/math/sparse_matrix.h" -#include "fesa/math/vector.h" - -namespace fesa { - -// Assembles only semantic nodal CLOAD records and forms the eliminated free -// right-hand side; stiffness factorization remains an analysis responsibility. -class LoadAssembler { -public: - static Result assembleFullNodalLoad( - const AnalysisModel& model, - const DofManager& dofs); - static Result effectiveFreeRhs( - const Vector& fullLoad, - const SparseMatrix& kfc, - const Vector& prescribedValues, - const DofManager& dofs); -}; - -} // namespace fesa diff --git a/include/fesa/assembly/parallel_for.h b/include/fesa/assembly/parallel_for.h new file mode 100644 index 0000000..165bb1b --- /dev/null +++ b/include/fesa/assembly/parallel_for.h @@ -0,0 +1,39 @@ +#ifndef FESA_ASSEMBLY_PARALLEL_FOR_H_ +#define FESA_ASSEMBLY_PARALLEL_FOR_H_ + +#include +#include + +namespace fesa { + +/// @brief Executes independent index-addressed work behind a backend boundary. +/// @note Callers own output storage and each invocation may write only its +/// index-owned slot. +class ParallelFor { + public: + virtual ~ParallelFor() = default; + + /// @brief Invokes body once for every index in [0, count). + virtual void Execute(std::size_t count, + const std::function& body) const = 0; +}; + +/// @brief Executes index-addressed work serially. +class SerialParallelFor final : public ParallelFor { + public: + /// @copydoc ParallelFor::Execute + void Execute(std::size_t count, + const std::function& body) const override; +}; + +/// @brief Executes index-addressed work through oneTBB. +class TbbParallelFor final : public ParallelFor { + public: + /// @copydoc ParallelFor::Execute + void Execute(std::size_t count, + const std::function& body) const override; +}; + +} // namespace fesa + +#endif // FESA_ASSEMBLY_PARALLEL_FOR_H_ diff --git a/include/fesa/assembly/parallel_for.hpp b/include/fesa/assembly/parallel_for.hpp deleted file mode 100644 index 59c7402..0000000 --- a/include/fesa/assembly/parallel_for.hpp +++ /dev/null @@ -1,32 +0,0 @@ -#pragma once - -#include -#include - -namespace fesa { - -// Executes independent index-addressed work without exposing the backend. -// Callers own output storage and must confine each invocation to its index. -class ParallelFor { -public: - virtual ~ParallelFor() = default; - virtual void execute( - std::size_t count, - const std::function& body) const = 0; -}; - -class SerialParallelFor final : public ParallelFor { -public: - void execute( - std::size_t count, - const std::function& body) const override; -}; - -class TbbParallelFor final : public ParallelFor { -public: - void execute( - std::size_t count, - const std::function& body) const override; -}; - -} // namespace fesa diff --git a/include/fesa/assembly/sparse_assembler.h b/include/fesa/assembly/sparse_assembler.h new file mode 100644 index 0000000..1bf8a76 --- /dev/null +++ b/include/fesa/assembly/sparse_assembler.h @@ -0,0 +1,27 @@ +#ifndef FESA_ASSEMBLY_SPARSE_ASSEMBLER_H_ +#define FESA_ASSEMBLY_SPARSE_ASSEMBLER_H_ + +#include "fesa/core/status.h" +#include "fesa/math/sparse_matrix.h" + +namespace fesa { + +class AnalysisModel; +class DofManager; +class ParallelFor; + +/// @brief Owns deterministic element-contribution reduction into global CSR. +class SparseAssembler { + public: + /// @brief Assembles stiffness in stable source-element and local-entry order. + /// @return A validated full-DOF CSR matrix or structured model failure. + /// @note Parallel workers produce index-owned local buffers; serial reduction + /// remains the sole global CSR writer. + static Result AssembleStiffness( + const AnalysisModel& model, const DofManager& dofs, + const ParallelFor& parallel_for); +}; + +} // namespace fesa + +#endif // FESA_ASSEMBLY_SPARSE_ASSEMBLER_H_ diff --git a/include/fesa/assembly/sparse_assembler.hpp b/include/fesa/assembly/sparse_assembler.hpp deleted file mode 100644 index 92bceda..0000000 --- a/include/fesa/assembly/sparse_assembler.hpp +++ /dev/null @@ -1,20 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" -#include "fesa/math/sparse_matrix.h" - -namespace fesa { - -class AnalysisModel; -class DofManager; -class ParallelFor; - -class SparseAssembler { -public: - static Result assembleStiffness( - const AnalysisModel& model, - const DofManager& dofs, - const ParallelFor& parallelFor); -}; - -} // namespace fesa diff --git a/include/fesa/constraints/essential_constraints.h b/include/fesa/constraints/essential_constraints.h new file mode 100644 index 0000000..24d06c7 --- /dev/null +++ b/include/fesa/constraints/essential_constraints.h @@ -0,0 +1,41 @@ +#ifndef FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINTS_H_ +#define FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINTS_H_ + +#include "fesa/core/status.h" +#include "fesa/math/sparse_matrix.h" +#include "fesa/math/vector.h" + +namespace fesa { + +class DofManager; + +/// @brief Stores full-stiffness blocks in stable free/constrained order. +struct PartitionedStiffness { + SparseMatrix kff; + SparseMatrix kfc; + SparseMatrix kcf; + SparseMatrix kcc; +}; + +/// @brief Applies stable prescribed-displacement elimination. +class EssentialConstraints { + public: + /// @brief Partitions full stiffness into Kff, Kfc, Kcf, and Kcc. + static Result Partition(const SparseMatrix& full, + const DofManager& dofs); + + /// @brief Gathers a full vector in stable free-equation order. + static Vector GatherFree(const Vector& full, const DofManager& dofs); + + /// @brief Gathers a full vector in stable constrained-DOF order. + static Vector GatherConstrained(const Vector& full, const DofManager& dofs); + + /// @brief Reconstructs full d from stable df and exact prescribed dc. + static Vector ReconstructFull(const Vector& free_values, + const Vector& constrained_values, + const DofManager& dofs); +}; + +} // namespace fesa + +#endif // FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINTS_H_ diff --git a/include/fesa/constraints/essential_constraints.hpp b/include/fesa/constraints/essential_constraints.hpp deleted file mode 100644 index f8072d2..0000000 --- a/include/fesa/constraints/essential_constraints.hpp +++ /dev/null @@ -1,35 +0,0 @@ -#pragma once - -#include "fesa/core/status.h" -#include "fesa/math/sparse_matrix.h" -#include "fesa/math/vector.h" - -namespace fesa { - -class DofManager; - -struct PartitionedStiffness { - SparseMatrix kff; - SparseMatrix kfc; - SparseMatrix kcf; - SparseMatrix kcc; -}; - -// Applies the DofManager's stable elimination order without owning equation -// numbering, load assembly, or a solver policy. -class EssentialConstraints { -public: - static Result partition( - const SparseMatrix& full, - const DofManager& dofs); - static Vector gatherFree(const Vector& full, const DofManager& dofs); - static Vector gatherConstrained( - const Vector& full, - const DofManager& dofs); - static Vector reconstructFull( - const Vector& freeValues, - const Vector& constrainedValues, - const DofManager& dofs); -}; - -} // namespace fesa diff --git a/include/fesa/fem/dof_manager.h b/include/fesa/fem/dof_manager.h new file mode 100644 index 0000000..3f1446c --- /dev/null +++ b/include/fesa/fem/dof_manager.h @@ -0,0 +1,83 @@ +#ifndef FESA_FEM_DOF_MANAGER_H_ +#define FESA_FEM_DOF_MANAGER_H_ + +#include +#include +#include +#include +#include + +#include "fesa/analysis/analysis_model.h" +#include "fesa/math/vector.h" + +namespace fesa { + +/// @brief Identifies one component in the stable six-DOF node layout. +enum class DofComponent : std::uint8_t { + kUx, + kUy, + kUz, + kUrx, + kUry, + kUrz, +}; + +/// @brief Stores the stable structural CSR pattern. +struct SparsePattern { + std::vector row_offsets; + std::vector column_indices; +}; + +/// @brief Owns full/free/constrained numbering, scatter maps, and CSR pattern. +class DofManager { + public: + /// @brief Creates every equation-space mapping for an active model. + static Result Create(const AnalysisModel& model); + + /// @brief Returns the full node-by-component DOF count. + std::size_t FullDofCount() const noexcept; + /// @brief Returns the free-equation count. + std::size_t FreeDofCount() const noexcept; + /// @brief Returns the prescribed-DOF count. + std::size_t ConstrainedDofCount() const noexcept; + /// @brief Maps a stable node index and component to a full DOF. + std::size_t FullDof(EntityIndex node, DofComponent component) const; + /// @brief Returns the free equation for a full DOF when unconstrained. + std::optional FreeEquation(std::size_t full_dof) const; + /// @brief Returns a beam scatter in endpoint/component order. + const std::array& ElementScatter(EntityIndex element) const; + /// @brief Returns a shell scatter in node/component order. + const std::array& ShellElementScatter( + EntityIndex element) const; + /// @brief Returns free full DOFs in stable increasing order. + const std::vector& FreeDofs() const noexcept; + /// @brief Returns constrained full DOFs in stable increasing order. + const std::vector& ConstrainedDofs() const noexcept; + /// @brief Returns dc in constrained-DOF order. + const Vector& PrescribedValues() const noexcept; + /// @brief Returns the full-space structural CSR pattern. + const SparsePattern& GetSparsePattern() const noexcept; + + private: + /// @brief Takes ownership of fully validated stable equation mappings. + DofManager(std::size_t full_dof_count, + std::vector> free_equations, + std::vector> element_scatters, + std::vector> shell_element_scatters, + std::vector free_dofs, + std::vector constrained_dofs, + Vector prescribed_values, SparsePattern sparse_pattern); + + std::size_t full_dof_count_; + std::vector> free_equations_; + std::vector> element_scatters_; + std::vector> shell_element_scatters_; + std::vector free_dofs_; + std::vector constrained_dofs_; + Vector prescribed_values_; + SparsePattern sparse_pattern_; +}; + +} // namespace fesa + +#endif // FESA_FEM_DOF_MANAGER_H_ diff --git a/include/fesa/fem/dof_manager.hpp b/include/fesa/fem/dof_manager.hpp deleted file mode 100644 index b6bcd65..0000000 --- a/include/fesa/fem/dof_manager.hpp +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/math/vector.h" - -#include -#include -#include -#include -#include - -namespace fesa { - -enum class DofComponent : std::uint8_t { - ux, - uy, - uz, - urx, - ury, - urz -}; - -struct SparsePattern { - std::vector rowOffsets; - std::vector columnIndices; -}; - -// Owns every equation-space mapping so semantic model objects remain free of -// analysis-specific equation IDs. -class DofManager { -public: - static Result create(const AnalysisModel& model); - - std::size_t fullDofCount() const noexcept; - std::size_t freeDofCount() const noexcept; - std::size_t constrainedDofCount() const noexcept; - std::size_t fullDof(EntityIndex node, DofComponent component) const; - std::optional freeEquation(std::size_t fullDof) const; - const std::array& elementScatter( - EntityIndex element) const; - const std::array& shellElementScatter( - EntityIndex element) const; - const std::vector& freeDofs() const noexcept; - const std::vector& constrainedDofs() const noexcept; - const Vector& prescribedValues() const noexcept; - const SparsePattern& sparsePattern() const noexcept; - -private: - DofManager( - std::size_t fullDofCount, - std::vector> freeEquations, - std::vector> elementScatters, - std::vector> shellElementScatters, - std::vector freeDofs, - std::vector constrainedDofs, - Vector prescribedValues, - SparsePattern sparsePattern); - - std::size_t fullDofCount_; - std::vector> freeEquations_; - std::vector> elementScatters_; - std::vector> shellElementScatters_; - std::vector freeDofs_; - std::vector constrainedDofs_; - Vector prescribedValues_; - SparsePattern sparsePattern_; -}; - -} // namespace fesa diff --git a/include/fesa/io/hdf5/hdf5_results_writer.hpp b/include/fesa/io/hdf5/hdf5_results_writer.hpp index 89fa817..35ab6d1 100644 --- a/include/fesa/io/hdf5/hdf5_results_writer.hpp +++ b/include/fesa/io/hdf5/hdf5_results_writer.hpp @@ -1,13 +1,13 @@ #pragma once -#include "fesa/results/results_writer.hpp" +#include "fesa/results/results_writer.h" namespace fesa { // Writes schema-v0 output while keeping backend and platform types private. class Hdf5ResultsWriter final : public ResultsWriter { public: - Status write( + Status Write( const std::filesystem::path& outputPath, const Domain& domain, const AnalysisState& state, diff --git a/include/fesa/results/result_records.h b/include/fesa/results/result_records.h new file mode 100644 index 0000000..f41c022 --- /dev/null +++ b/include/fesa/results/result_records.h @@ -0,0 +1,86 @@ +#ifndef FESA_RESULTS_RESULT_RECORDS_H_ +#define FESA_RESULTS_RESULT_RECORDS_H_ + +#include +#include +#include +#include + +#include "fesa/model/model_types.h" + +namespace fesa { + +/// @brief Identifies one deterministic result frame. +struct StepFrameIdentity { + std::string step_name; + std::size_t frame_index; +}; + +/// @brief Stores one beam endpoint action and section-resultant row. +struct EndpointResultRow { + EntityIndex element; + int endpoint; + SourceEntityId node; + std::array end_action; + std::array section_resultant; +}; + +/// @brief Stores one beam Gauss generalized result row. +struct GaussResultRow { + EntityIndex element; + int gauss_point; + std::array generalized_strain; + std::array generalized_resultant; +}; + +/// @brief Stores one beam axial-stress section-point row. +struct StressS11Row { + EntityIndex element; + int gauss_point; + std::size_t section_point; + double x1; + double x2; + double s11; + std::string source; +}; + +/// @brief Identifies one MITC4 midsurface integration location. +enum class ShellMidsurfaceLocation { kGp1, kGp2, kGp3, kGp4 }; + +/// @brief Identifies one through-thickness shell recovery position. +enum class ShellSectionPosition { kBottom, kMiddle, kTop }; + +/// @brief Stores one through-thickness shell stress row. +struct ShellSectionStressRow { + ShellSectionPosition position; + double zeta; + std::array components; +}; + +/// @brief Stores one MITC4 physical recovery row. +struct ShellResultRow { + EntityIndex element; + ShellMidsurfaceLocation location; + std::array natural_coordinates; + // Axis rows [e1,e2,e3], global-component columns. + std::array, 3> local_frame; + std::array generalized_strain; + std::array section_resultant; + // Fixed BOTTOM, MIDDLE, TOP order; components are [S11,S22,S12]. + std::array stress; +}; + +/// @brief Carries a complete shell result candidate for atomic validation. +struct ShellStateCandidate { + std::vector rows; + double physical_strain_energy{0.0}; + // [FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3]. + std::array equilibrium{}; + // [FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED, + // MOMENT_BALANCE_NORMALIZED]. + std::array verification_metrics{}; +}; + +} // namespace fesa + +#endif // FESA_RESULTS_RESULT_RECORDS_H_ diff --git a/include/fesa/results/result_records.hpp b/include/fesa/results/result_records.hpp deleted file mode 100644 index d8d0850..0000000 --- a/include/fesa/results/result_records.hpp +++ /dev/null @@ -1,83 +0,0 @@ -#pragma once - -#include "fesa/model/model_types.h" - -#include -#include -#include -#include - -namespace fesa { - -struct StepFrameIdentity { - std::string stepName; - std::size_t frameIndex; -}; - -struct EndpointResultRow { - EntityIndex element; - int endpoint; - SourceEntityId node; - std::array endAction; - std::array sectionResultant; -}; - -struct GaussResultRow { - EntityIndex element; - int gaussPoint; - std::array generalizedStrain; - std::array generalizedResultant; -}; - -struct StressS11Row { - EntityIndex element; - int gaussPoint; - std::size_t sectionPoint; - double x1; - double x2; - double s11; - std::string source; -}; - -enum class ShellMidsurfaceLocation { - gp1, - gp2, - gp3, - gp4 -}; - -enum class ShellSectionPosition { - bottom, - middle, - top -}; - -struct ShellSectionStressRow { - ShellSectionPosition position; - double zeta; - std::array components; -}; - -struct ShellResultRow { - EntityIndex element; - ShellMidsurfaceLocation location; - std::array naturalCoordinates; - // Axis rows [e1,e2,e3], global-component columns. - std::array, 3> localFrame; - std::array generalizedStrain; - std::array sectionResultant; - // Fixed BOTTOM, MIDDLE, TOP order; components are [S11,S22,S12]. - std::array stress; -}; - -struct ShellStateCandidate { - std::vector rows; - double physicalStrainEnergy{0.0}; - // [FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3]. - std::array equilibrium{}; - // [FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED, - // MOMENT_BALANCE_NORMALIZED]. - std::array verificationMetrics{}; -}; - -} // namespace fesa diff --git a/include/fesa/results/result_recovery.h b/include/fesa/results/result_recovery.h new file mode 100644 index 0000000..2cb797e --- /dev/null +++ b/include/fesa/results/result_recovery.h @@ -0,0 +1,43 @@ +#ifndef FESA_RESULTS_RESULT_RECOVERY_H_ +#define FESA_RESULTS_RESULT_RECOVERY_H_ + +#include +#include + +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/core/status.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/math/sparse_matrix.h" + +namespace fesa { + +/// @brief Stores one normalized positive-local-x node-station row. +struct NodeStationResultRow { + SourceEntityId node; + EntityIndex representative_element; + std::array section_resultant; +}; + +/// @brief Recovers full equilibrium and active concrete element rows. +class ResultRecovery { + public: + /// @brief Builds and atomically commits a complete recovery candidate. + /// @return Success after full residual K*d-F and all result rows validate. + static Status Recover(const AnalysisModel& model, const DofManager& dofs, + const SparseMatrix& full_stiffness, + AnalysisState& state); + + /// @brief Normalizes eligible endpoint rows to source node stations. + /// @param component_tolerances Per-component interior-station tolerances. + /// @return Stable source-node rows or a structured identity/value failure. + static Result> + NormalizeSectionResultantsToNodeStations( + const AnalysisModel& model, + const std::vector& endpoint_rows, + const std::array& component_tolerances); +}; + +} // namespace fesa + +#endif // FESA_RESULTS_RESULT_RECOVERY_H_ diff --git a/include/fesa/results/result_recovery.hpp b/include/fesa/results/result_recovery.hpp deleted file mode 100644 index 370a031..0000000 --- a/include/fesa/results/result_recovery.hpp +++ /dev/null @@ -1,36 +0,0 @@ -#pragma once - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/core/status.h" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/math/sparse_matrix.h" - -#include -#include - -namespace fesa { - -struct NodeStationResultRow { - SourceEntityId node; - EntityIndex representativeElement; - std::array sectionResultant; -}; - -// Recovers full-space equilibrium and the active concrete element rows without -// exposing element or sparse-backend details to result consumers. -class ResultRecovery { -public: - static Status recover(const AnalysisModel& model, - const DofManager& dofs, - const SparseMatrix& fullStiffness, - AnalysisState& state); - - static Result> - normalizeSectionResultantsToNodeStations( - const AnalysisModel& model, - const std::vector& endpointRows, - const std::array& componentTolerances); -}; - -} // namespace fesa diff --git a/include/fesa/results/results_writer.h b/include/fesa/results/results_writer.h new file mode 100644 index 0000000..d227554 --- /dev/null +++ b/include/fesa/results/results_writer.h @@ -0,0 +1,28 @@ +#ifndef FESA_RESULTS_RESULTS_WRITER_H_ +#define FESA_RESULTS_RESULTS_WRITER_H_ + +#include +#include + +#include "fesa/analysis/analysis_state.h" +#include "fesa/core/diagnostic.h" +#include "fesa/core/status.h" +#include "fesa/model/domain.h" + +namespace fesa { + +/// @brief Isolates authoritative result storage from the solver core. +class ResultsWriter { + public: + virtual ~ResultsWriter() = default; + + /// @brief Writes one complete validated analysis state. + /// @return Success only after backend-specific finalization completes. + virtual Status Write(const std::filesystem::path& output_path, + const Domain& domain, const AnalysisState& state, + const std::vector& diagnostics) = 0; +}; + +} // namespace fesa + +#endif // FESA_RESULTS_RESULTS_WRITER_H_ diff --git a/include/fesa/results/results_writer.hpp b/include/fesa/results/results_writer.hpp deleted file mode 100644 index 40e4ef2..0000000 --- a/include/fesa/results/results_writer.hpp +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once - -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/core/diagnostic.h" -#include "fesa/core/status.h" -#include "fesa/model/domain.h" - -#include -#include - -namespace fesa { - -// Keeps the solver core independent of the authoritative result-storage backend. -class ResultsWriter { -public: - virtual ~ResultsWriter() = default; - - virtual Status write( - const std::filesystem::path& outputPath, - const Domain& domain, - const AnalysisState& state, - const std::vector& diagnostics) = 0; -}; - -} // namespace fesa diff --git a/src/fesa/analysis/analysis_model.cpp b/src/fesa/analysis/analysis_model.cpp index 8b13514..7116181 100644 --- a/src/fesa/analysis/analysis_model.cpp +++ b/src/fesa/analysis/analysis_model.cpp @@ -1,96 +1,90 @@ -#include "fesa/analysis/analysis_model.hpp" +#include "fesa/analysis/analysis_model.h" #include #include namespace fesa { -Result AnalysisModel::create(const Domain& domain) { - if (domain.Steps().empty()) { - return Result::Failure(Status::Failure( - FailureCategory::kInput, - {{Severity::kError, - "invalid-model-cardinality", - {domain.SourcePath(), 0U}, - "STEP", - "0", - "AnalysisModel requires exactly one static step."}})); - } - if (domain.Steps().size() > 1U) { - const auto& secondStep = domain.Steps()[1]; - return Result::Failure(Status::Failure( - FailureCategory::kInput, - {{Severity::kError, - "unsupported-multiple-step", - secondStep.location, - "STEP", - secondStep.name, - "AnalysisModel does not support multiple steps."}})); - } - return Result::Success(AnalysisModel{domain}); +Result AnalysisModel::Create(const Domain& domain) { + if (domain.Steps().empty()) { + return Result::Failure( + Status::Failure(FailureCategory::kInput, + {{Severity::kError, + "invalid-model-cardinality", + {domain.SourcePath(), 0U}, + "STEP", + "0", + "AnalysisModel requires exactly one static step."}})); + } + if (domain.Steps().size() > 1U) { + const auto& second_step = domain.Steps()[1]; + return Result::Failure( + Status::Failure(FailureCategory::kInput, + {{Severity::kError, "unsupported-multiple-step", + second_step.location, "STEP", second_step.name, + "AnalysisModel does not support multiple steps."}})); + } + return Result::Success(AnalysisModel{domain}); } -const Domain& AnalysisModel::domain() const noexcept { - return *domain_; +const Domain& AnalysisModel::GetDomain() const noexcept { return *domain_; } + +const StaticStepDefinition& AnalysisModel::Step() const noexcept { + return domain_->Steps().front(); } -const StaticStepDefinition& AnalysisModel::step() const noexcept { - return domain_->Steps().front(); +const std::vector& AnalysisModel::ActiveElements() const noexcept { + return active_elements_; } -const std::vector& AnalysisModel::activeElements() const noexcept { - return activeElements_; +const std::vector& AnalysisModel::ActiveMaterials() + const noexcept { + return active_materials_; } -const std::vector& AnalysisModel::activeMaterials() const noexcept { - return activeMaterials_; +const std::vector& AnalysisModel::ActiveSections() const noexcept { + return active_sections_; } -const std::vector& AnalysisModel::activeSections() const noexcept { - return activeSections_; +const std::vector& AnalysisModel::ActiveBoundaryConditions() + const noexcept { + return active_boundary_conditions_; } -const std::vector& -AnalysisModel::activeBoundaryConditions() const noexcept { - return activeBoundaryConditions_; -} - -const std::vector& AnalysisModel::activeLoads() const noexcept { - return activeLoads_; +const std::vector& AnalysisModel::ActiveLoads() const noexcept { + return active_loads_; } AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} { - std::vector reachableMaterials(domain.Materials().size(), false); - std::vector reachableSections(domain.Sections().size(), false); + std::vector reachable_materials(domain.Materials().size(), false); + std::vector reachable_sections(domain.Sections().size(), false); - for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { - const auto& element = domain.Elements()[index]; - activeElements_.push_back(static_cast(index)); - reachableMaterials[element.material_index] = true; - reachableSections[element.section_index] = true; - } + for (std::size_t index = 0U; index < domain.Elements().size(); ++index) { + const auto& element = domain.Elements()[index]; + active_elements_.push_back(static_cast(index)); + reachable_materials[element.material_index] = true; + reachable_sections[element.section_index] = true; + } - // Ascending vector positions are the stable internal order, independent - // of first reachability or duplicate element assignments. - for (std::size_t index = 0U; index < reachableMaterials.size(); ++index) { - if (reachableMaterials[index]) { - activeMaterials_.push_back(static_cast(index)); - } + // Ascending vector positions are the stable internal order, independent + // of first reachability or duplicate element assignments. + for (std::size_t index = 0U; index < reachable_materials.size(); ++index) { + if (reachable_materials[index]) { + active_materials_.push_back(static_cast(index)); } - for (std::size_t index = 0U; index < reachableSections.size(); ++index) { - if (reachableSections[index]) { - activeSections_.push_back(static_cast(index)); - } + } + for (std::size_t index = 0U; index < reachable_sections.size(); ++index) { + if (reachable_sections[index]) { + active_sections_.push_back(static_cast(index)); } + } - for (std::size_t index = 0U; - index < step().boundaries.size(); - ++index) { - activeBoundaryConditions_.push_back(static_cast(index)); - } - for (std::size_t index = 0U; index < step().loads.size(); ++index) { - activeLoads_.push_back(static_cast(index)); - } + for (std::size_t index = 0U; index < Step().boundaries.size(); ++index) { + active_boundary_conditions_.push_back(static_cast(index)); + } + for (std::size_t index = 0U; index < Step().loads.size(); ++index) { + active_loads_.push_back(static_cast(index)); + } } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/analysis/analysis_state.cpp b/src/fesa/analysis/analysis_state.cpp index c3f5f5c..eb82384 100644 --- a/src/fesa/analysis/analysis_state.cpp +++ b/src/fesa/analysis/analysis_state.cpp @@ -1,4 +1,4 @@ -#include "fesa/analysis/analysis_state.hpp" +#include "fesa/analysis/analysis_state.h" #include #include @@ -12,244 +12,212 @@ namespace { constexpr std::size_t kShellLocationsPerElement = 4U; -Status shellCandidateFailure( - const std::string& code, - const std::string& identity, - const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, - code, - {}, - "ANALYSIS_STATE", - identity, - message}}); +/// @brief Creates a structured failure without mutating existing state. +Status ShellCandidateFailure(const std::string& code, + const std::string& identity, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, {}, "ANALYSIS_STATE", identity, message}}); } -template -bool finite(const std::array& values) { - return std::all_of( - values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); +template +/// @brief Tests one fixed-size candidate component 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); }); } -bool finite(const ShellResultRow& row) { - if (!finite(row.naturalCoordinates) || - !finite(row.generalizedStrain) || - !finite(row.sectionResultant)) { - return false; +/// @brief Tests every physical shell row component for finite values. +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; } - for (const auto& axis : row.localFrame) { - if (!finite(axis)) { - return false; - } - } - return std::all_of( - row.stress.begin(), row.stress.end(), - [](const ShellSectionStressRow& stress) { - return std::isfinite(stress.zeta) && finite(stress.components); - }); + } + return std::all_of(row.stress.begin(), row.stress.end(), + [](const ShellSectionStressRow& stress) { + return std::isfinite(stress.zeta) && + IsFinite(stress.components); + }); } -} // namespace +} // namespace -AnalysisState AnalysisState::create( - const DofManager& dofs, StepFrameIdentity identity) { - return AnalysisState{dofs.fullDofCount(), std::move(identity)}; +AnalysisState AnalysisState::Create(const DofManager& dofs, + StepFrameIdentity identity) { + return AnalysisState{dofs.FullDofCount(), std::move(identity)}; } -Vector& AnalysisState::displacement() noexcept { - return displacement_; +Vector& AnalysisState::Displacement() noexcept { return displacement_; } + +const Vector& AnalysisState::Displacement() const noexcept { + return displacement_; } -const Vector& AnalysisState::displacement() const noexcept { - return displacement_; +Vector& AnalysisState::ExternalForce() noexcept { return external_force_; } + +const Vector& AnalysisState::ExternalForce() const noexcept { + return external_force_; } -Vector& AnalysisState::externalForce() noexcept { - return externalForce_; +Vector& AnalysisState::InternalForce() noexcept { return internal_force_; } + +const Vector& AnalysisState::InternalForce() const noexcept { + return internal_force_; } -const Vector& AnalysisState::externalForce() const noexcept { - return externalForce_; +Vector& AnalysisState::Residual() noexcept { return residual_; } + +const Vector& AnalysisState::Residual() const noexcept { return residual_; } + +Vector& AnalysisState::Reaction() noexcept { return reaction_; } + +const Vector& AnalysisState::Reaction() const noexcept { return reaction_; } + +const StepFrameIdentity& AnalysisState::Identity() const noexcept { + return identity_; } -Vector& AnalysisState::internalForce() noexcept { - return internalForce_; +std::vector& AnalysisState::EndpointResults() noexcept { + return endpoint_results_; } -const Vector& AnalysisState::internalForce() const noexcept { - return internalForce_; +const std::vector& AnalysisState::EndpointResults() + const noexcept { + return endpoint_results_; } -Vector& AnalysisState::residual() noexcept { - return residual_; +std::vector& AnalysisState::GaussResults() noexcept { + return gauss_results_; } -const Vector& AnalysisState::residual() const noexcept { - return residual_; +const std::vector& AnalysisState::GaussResults() + const noexcept { + return gauss_results_; } -Vector& AnalysisState::reaction() noexcept { - return reaction_; +std::vector& AnalysisState::StressResults() noexcept { + return stress_results_; } -const Vector& AnalysisState::reaction() const noexcept { - return reaction_; +const std::vector& AnalysisState::StressResults() const noexcept { + return stress_results_; } -const StepFrameIdentity& AnalysisState::identity() const noexcept { - return identity_; -} - -std::vector& AnalysisState::endpointResults() noexcept { - return endpointResults_; -} - -const std::vector& AnalysisState::endpointResults() const noexcept { - return endpointResults_; -} - -std::vector& AnalysisState::gaussResults() noexcept { - return gaussResults_; -} - -const std::vector& AnalysisState::gaussResults() const noexcept { - return gaussResults_; -} - -std::vector& AnalysisState::stressResults() noexcept { - return stressResults_; -} - -const std::vector& AnalysisState::stressResults() const noexcept { - return stressResults_; -} - -Status AnalysisState::commitShellResults( - const std::vector& expectedElementOrder, +Status AnalysisState::CommitShellResults( + const std::vector& expected_element_order, ShellStateCandidate candidate) { - if (expectedElementOrder.size() > - (std::numeric_limits::max)() / - kShellLocationsPerElement) { - return shellCandidateFailure( - "invalid-shell-state-inventory", - identity_.stepName, - "The expected shell result inventory is too large."); - } - const std::size_t expectedRowCount = - expectedElementOrder.size() * kShellLocationsPerElement; - if (candidate.rows.size() != expectedRowCount) { - return shellCandidateFailure( - "invalid-shell-state-inventory", - identity_.stepName, - "Shell results require exactly four rows per expected element."); - } - if (std::adjacent_find( - expectedElementOrder.begin(), expectedElementOrder.end(), - [](const EntityIndex left, const EntityIndex right) { - return left >= right; - }) != expectedElementOrder.end()) { - return shellCandidateFailure( - "invalid-shell-state-inventory", - identity_.stepName, - "Expected shell elements must be unique and in stable index order."); - } + if (expected_element_order.size() > + (std::numeric_limits::max)() / kShellLocationsPerElement) { + return ShellCandidateFailure( + "invalid-shell-state-inventory", identity_.step_name, + "The expected shell result inventory is too large."); + } + const std::size_t expected_row_count = + expected_element_order.size() * kShellLocationsPerElement; + if (candidate.rows.size() != expected_row_count) { + return ShellCandidateFailure( + "invalid-shell-state-inventory", identity_.step_name, + "Shell results require exactly four rows per expected element."); + } + if (std::adjacent_find(expected_element_order.begin(), + expected_element_order.end(), + [](const EntityIndex left, const EntityIndex right) { + return left >= right; + }) != expected_element_order.end()) { + return ShellCandidateFailure( + "invalid-shell-state-inventory", identity_.step_name, + "Expected shell elements must be unique and in stable index order."); + } - const std::array - expectedLocations{ - ShellMidsurfaceLocation::gp1, - ShellMidsurfaceLocation::gp2, - ShellMidsurfaceLocation::gp3, - ShellMidsurfaceLocation::gp4}; - const double gauss = 1.0 / std::sqrt(3.0); - const std::array, kShellLocationsPerElement> - expectedCoordinates{ - std::array{-gauss, -gauss}, - std::array{gauss, -gauss}, - std::array{gauss, gauss}, - std::array{-gauss, gauss}}; - const std::array expectedPositions{ - ShellSectionPosition::bottom, - ShellSectionPosition::middle, - ShellSectionPosition::top}; - constexpr std::array expectedZeta{-1.0, 0.0, 1.0}; - for (std::size_t elementOrder = 0U; - elementOrder < expectedElementOrder.size(); - ++elementOrder) { - for (std::size_t point = 0U; - point < kShellLocationsPerElement; - ++point) { - const auto& row = candidate.rows[ - elementOrder * kShellLocationsPerElement + point]; - if (row.element != expectedElementOrder[elementOrder] || - row.location != expectedLocations[point] || - row.naturalCoordinates != expectedCoordinates[point]) { - return shellCandidateFailure( - "invalid-shell-state-inventory", - std::to_string(row.element), - "Shell rows must preserve element and GP1 through GP4 identity."); - } - for (std::size_t position = 0U; - position < expectedPositions.size(); - ++position) { - if (row.stress[position].position != - expectedPositions[position] || - row.stress[position].zeta != expectedZeta[position]) { - return shellCandidateFailure( - "invalid-shell-state-inventory", - std::to_string(row.element), - "Shell stress rows require BOTTOM, MIDDLE, TOP identity."); - } - } - if (!finite(row)) { - return shellCandidateFailure( - "nonfinite-shell-state-value", - std::to_string(row.element), - "Shell result rows must contain only finite values."); - } + const std::array + expected_locations{ + ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2, + ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4}; + const double gauss = 1.0 / std::sqrt(3.0); + 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_order = 0U; + element_order < expected_element_order.size(); ++element_order) { + for (std::size_t point = 0U; point < kShellLocationsPerElement; ++point) { + const auto& row = + candidate.rows[element_order * kShellLocationsPerElement + point]; + if (row.element != expected_element_order[element_order] || + row.location != expected_locations[point] || + row.natural_coordinates != expected_coordinates[point]) { + return ShellCandidateFailure( + "invalid-shell-state-inventory", std::to_string(row.element), + "Shell rows must preserve element and GP1 through GP4 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 ShellCandidateFailure( + "invalid-shell-state-inventory", std::to_string(row.element), + "Shell stress rows require BOTTOM, MIDDLE, TOP identity."); } + } + if (!IsFinite(row)) { + return ShellCandidateFailure( + "nonfinite-shell-state-value", std::to_string(row.element), + "Shell result rows must contain only finite values."); + } } + } - if (!std::isfinite(candidate.physicalStrainEnergy) || - !finite(candidate.equilibrium) || - !finite(candidate.verificationMetrics)) { - return shellCandidateFailure( - "nonfinite-shell-state-value", - identity_.stepName, - "Shell energy, equilibrium, and normalized metrics must be finite."); - } + if (!std::isfinite(candidate.physical_strain_energy) || + !IsFinite(candidate.equilibrium) || + !IsFinite(candidate.verification_metrics)) { + return ShellCandidateFailure( + "nonfinite-shell-state-value", identity_.step_name, + "Shell energy, equilibrium, and normalized metrics must be finite."); + } - shellResults_ = std::move(candidate.rows); - physicalStrainEnergy_ = candidate.physicalStrainEnergy; - equilibrium_ = candidate.equilibrium; - verificationMetrics_ = candidate.verificationMetrics; - return Status::Ok(); + shell_results_ = std::move(candidate.rows); + physical_strain_energy_ = candidate.physical_strain_energy; + equilibrium_ = candidate.equilibrium; + verification_metrics_ = candidate.verification_metrics; + return Status::Ok(); } -const std::vector& AnalysisState::shellResults() const noexcept { - return shellResults_; +const std::vector& AnalysisState::ShellResults() + const noexcept { + return shell_results_; } -double AnalysisState::physicalStrainEnergy() const noexcept { - return physicalStrainEnergy_; +double AnalysisState::PhysicalStrainEnergy() const noexcept { + return physical_strain_energy_; } -const std::array& AnalysisState::equilibrium() const noexcept { - return equilibrium_; +const std::array& AnalysisState::Equilibrium() const noexcept { + return equilibrium_; } -const std::array& AnalysisState::verificationMetrics() const noexcept { - return verificationMetrics_; +const std::array& AnalysisState::VerificationMetrics() + const noexcept { + return verification_metrics_; } -AnalysisState::AnalysisState( - std::size_t fullDofCount, StepFrameIdentity identity) +AnalysisState::AnalysisState(std::size_t full_dof_count, + StepFrameIdentity identity) : identity_{std::move(identity)}, - displacement_{fullDofCount}, - externalForce_{fullDofCount}, - internalForce_{fullDofCount}, - residual_{fullDofCount}, - reaction_{fullDofCount} {} + displacement_{full_dof_count}, + external_force_{full_dof_count}, + internal_force_{full_dof_count}, + residual_{full_dof_count}, + reaction_{full_dof_count} {} -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/analysis/linear_static_analysis.cpp b/src/fesa/analysis/linear_static_analysis.cpp index 4e6a3f5..b603846 100644 --- a/src/fesa/analysis/linear_static_analysis.cpp +++ b/src/fesa/analysis/linear_static_analysis.cpp @@ -1,170 +1,166 @@ -#include "fesa/analysis/linear_static_analysis.hpp" - -#include "fesa/assembly/load_assembler.hpp" -#include "fesa/assembly/parallel_for.hpp" -#include "fesa/assembly/sparse_assembler.hpp" -#include "fesa/io/abaqus/domain_mapper.hpp" -#include "fesa/io/abaqus/input_reader.hpp" -#include "fesa/results/result_recovery.hpp" -#include "fesa/results/results_writer.hpp" -#include "fesa/solvers/linear/linear_solver.h" +#include "fesa/analysis/linear_static_analysis.h" #include +#include "fesa/assembly/load_assembler.h" +#include "fesa/assembly/parallel_for.h" +#include "fesa/assembly/sparse_assembler.h" +#include "fesa/io/abaqus/domain_mapper.hpp" +#include "fesa/io/abaqus/input_reader.hpp" +#include "fesa/results/result_recovery.h" +#include "fesa/results/results_writer.h" +#include "fesa/solvers/linear/linear_solver.h" + namespace fesa { -Status Analysis::run(const AnalysisRequest& request) { - Status status = initialize(request); - if (!status.IsOk()) { - return status; - } - status = buildAnalysisModel(); - if (!status.IsOk()) { - return status; - } - status = buildDofMapAndSparsePattern(); - if (!status.IsOk()) { - return status; - } - status = assembleAndPartitionStiffness(); - if (!status.IsOk()) { - return status; - } - status = factorize(); - if (!status.IsOk()) { - return status; - } - status = assembleLoadsAndEffectiveRhs(); - if (!status.IsOk()) { - return status; - } - status = substituteAndReconstruct(); - if (!status.IsOk()) { - return status; - } - return recoverAndWriteResults(); +Status Analysis::Run(const AnalysisRequest& request) { + Status status = Initialize(request); + if (!status.IsOk()) { + return status; + } + status = BuildAnalysisModel(); + if (!status.IsOk()) { + return status; + } + status = BuildDofMapAndSparsePattern(); + if (!status.IsOk()) { + return status; + } + status = AssembleAndPartitionStiffness(); + if (!status.IsOk()) { + return status; + } + status = Factorize(); + if (!status.IsOk()) { + return status; + } + status = AssembleLoadsAndEffectiveRhs(); + if (!status.IsOk()) { + return status; + } + status = SubstituteAndReconstruct(); + if (!status.IsOk()) { + return status; + } + return RecoverAndWriteResults(); } -LinearStaticAnalysis::LinearStaticAnalysis( - const ParallelFor& parallelFor, - LinearSolver& linearSolver, - ResultsWriter& resultsWriter) - : parallelFor_{parallelFor}, - linearSolver_{linearSolver}, - resultsWriter_{resultsWriter} {} +LinearStaticAnalysis::LinearStaticAnalysis(const ParallelFor& parallel_for, + LinearSolver& linear_solver, + ResultsWriter& results_writer) + : parallel_for_{parallel_for}, + linear_solver_{linear_solver}, + results_writer_{results_writer} {} -Status LinearStaticAnalysis::initialize(const AnalysisRequest& request) { - // Clear dependent objects in reverse ownership order so a reused analysis - // never exposes a view into a Domain from an earlier run. - effectiveRhs_.reset(); - partitionedStiffness_.reset(); - fullStiffness_.reset(); - state_.reset(); - dofs_.reset(); - model_.reset(); - domain_.reset(); - diagnostics_.clear(); - request_ = request; +Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) { + // Clear dependent objects in reverse ownership order so a reused analysis + // never exposes a view into a Domain from an earlier run. + effective_rhs_.reset(); + partitioned_stiffness_.reset(); + full_stiffness_.reset(); + state_.reset(); + dofs_.reset(); + model_.reset(); + domain_.reset(); + diagnostics_.clear(); + request_ = request; - const auto parsed = AbaqusInputReader{}.read(request_.inputPath); - if (!parsed.HasValue()) { - return parsed.GetStatus(); - } - auto domain = AbaqusDomainMapper{}.map(parsed.Value()); - if (!domain.HasValue()) { - return domain.GetStatus(); - } + const auto parsed = AbaqusInputReader{}.read(request_.input_path); + if (!parsed.HasValue()) { + return parsed.GetStatus(); + } + auto domain = AbaqusDomainMapper{}.map(parsed.Value()); + if (!domain.HasValue()) { + return domain.GetStatus(); + } - domain_ = std::make_unique(std::move(domain.Value())); - diagnostics_ = domain_->Warnings(); - SortDiagnostics(diagnostics_); - return Status::Ok(); + domain_ = std::make_unique(std::move(domain.Value())); + diagnostics_ = domain_->Warnings(); + SortDiagnostics(diagnostics_); + return Status::Ok(); } -Status LinearStaticAnalysis::buildAnalysisModel() { - auto model = AnalysisModel::create(*domain_); - if (!model.HasValue()) { - return model.GetStatus(); - } - model_ = std::make_unique(std::move(model.Value())); - return Status::Ok(); +Status LinearStaticAnalysis::BuildAnalysisModel() { + auto model = AnalysisModel::Create(*domain_); + if (!model.HasValue()) { + return model.GetStatus(); + } + model_ = std::make_unique(std::move(model.Value())); + return Status::Ok(); } -Status LinearStaticAnalysis::buildDofMapAndSparsePattern() { - auto dofs = DofManager::create(*model_); - if (!dofs.HasValue()) { - return dofs.GetStatus(); - } - dofs_ = std::make_unique(std::move(dofs.Value())); - state_ = std::make_unique( - AnalysisState::create(*dofs_, {"Step-1", 0U})); - return Status::Ok(); +Status LinearStaticAnalysis::BuildDofMapAndSparsePattern() { + auto dofs = DofManager::Create(*model_); + if (!dofs.HasValue()) { + return dofs.GetStatus(); + } + dofs_ = std::make_unique(std::move(dofs.Value())); + state_ = std::make_unique( + AnalysisState::Create(*dofs_, {"Step-1", 0U})); + return Status::Ok(); } -Status LinearStaticAnalysis::assembleAndPartitionStiffness() { - auto stiffness = SparseAssembler::assembleStiffness( - *model_, *dofs_, parallelFor_); - if (!stiffness.HasValue()) { - return stiffness.GetStatus(); - } - fullStiffness_ = - std::make_unique(std::move(stiffness.Value())); +Status LinearStaticAnalysis::AssembleAndPartitionStiffness() { + auto stiffness = + SparseAssembler::AssembleStiffness(*model_, *dofs_, parallel_for_); + if (!stiffness.HasValue()) { + return stiffness.GetStatus(); + } + full_stiffness_ = + std::make_unique(std::move(stiffness.Value())); - auto partitioned = EssentialConstraints::partition( - *fullStiffness_, *dofs_); - if (!partitioned.HasValue()) { - return partitioned.GetStatus(); - } - partitionedStiffness_ = std::make_unique( - std::move(partitioned.Value())); - return Status::Ok(); + auto partitioned = EssentialConstraints::Partition(*full_stiffness_, *dofs_); + if (!partitioned.HasValue()) { + return partitioned.GetStatus(); + } + partitioned_stiffness_ = + std::make_unique(std::move(partitioned.Value())); + return Status::Ok(); } -Status LinearStaticAnalysis::factorize() { - // This call intentionally precedes all load assembly in Analysis::run. - return linearSolver_.Factorize(partitionedStiffness_->kff); +Status LinearStaticAnalysis::Factorize() { + // This call intentionally precedes all load assembly in Analysis::Run. + return linear_solver_.Factorize(partitioned_stiffness_->kff); } -Status LinearStaticAnalysis::assembleLoadsAndEffectiveRhs() { - auto fullLoad = LoadAssembler::assembleFullNodalLoad(*model_, *dofs_); - if (!fullLoad.HasValue()) { - return fullLoad.GetStatus(); - } - state_->externalForce() = std::move(fullLoad.Value()); +Status LinearStaticAnalysis::AssembleLoadsAndEffectiveRhs() { + auto full_load = LoadAssembler::AssembleFullNodalLoad(*model_, *dofs_); + if (!full_load.HasValue()) { + return full_load.GetStatus(); + } + state_->ExternalForce() = std::move(full_load.Value()); - auto rhs = LoadAssembler::effectiveFreeRhs( - state_->externalForce(), - partitionedStiffness_->kfc, - dofs_->prescribedValues(), - *dofs_); - if (!rhs.HasValue()) { - return rhs.GetStatus(); - } - effectiveRhs_ = std::make_unique(std::move(rhs.Value())); - return Status::Ok(); + auto rhs = LoadAssembler::EffectiveFreeRhs(state_->ExternalForce(), + partitioned_stiffness_->kfc, + dofs_->PrescribedValues(), *dofs_); + if (!rhs.HasValue()) { + return rhs.GetStatus(); + } + effective_rhs_ = std::make_unique(std::move(rhs.Value())); + return Status::Ok(); } -Status LinearStaticAnalysis::substituteAndReconstruct() { - Vector freeDisplacement{dofs_->freeDofCount()}; - const Status solveStatus = - linearSolver_.Solve(*effectiveRhs_, freeDisplacement); - if (!solveStatus.IsOk()) { - return solveStatus; - } +Status LinearStaticAnalysis::SubstituteAndReconstruct() { + Vector free_displacement{dofs_->FreeDofCount()}; + const Status solve_status = + linear_solver_.Solve(*effective_rhs_, free_displacement); + if (!solve_status.IsOk()) { + return solve_status; + } - state_->displacement() = EssentialConstraints::reconstructFull( - freeDisplacement, dofs_->prescribedValues(), *dofs_); - return Status::Ok(); + state_->Displacement() = EssentialConstraints::ReconstructFull( + free_displacement, dofs_->PrescribedValues(), *dofs_); + return Status::Ok(); } -Status LinearStaticAnalysis::recoverAndWriteResults() { - const Status recoveryStatus = ResultRecovery::recover( - *model_, *dofs_, *fullStiffness_, *state_); - if (!recoveryStatus.IsOk()) { - return recoveryStatus; - } - return resultsWriter_.write( - request_.outputPath, *domain_, *state_, diagnostics_); +Status LinearStaticAnalysis::RecoverAndWriteResults() { + const Status recovery_status = + ResultRecovery::Recover(*model_, *dofs_, *full_stiffness_, *state_); + if (!recovery_status.IsOk()) { + return recovery_status; + } + return results_writer_.Write(request_.output_path, *domain_, *state_, + diagnostics_); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/app/fesa_application.cpp b/src/fesa/app/fesa_application.cpp index 6e0e240..7c8128a 100644 --- a/src/fesa/app/fesa_application.cpp +++ b/src/fesa/app/fesa_application.cpp @@ -1,7 +1,7 @@ #include "fesa/app/fesa_application.hpp" -#include "fesa/analysis/linear_static_analysis.hpp" -#include "fesa/assembly/parallel_for.hpp" +#include "fesa/analysis/linear_static_analysis.h" +#include "fesa/assembly/parallel_for.h" #include "fesa/core/diagnostic.h" #include "fesa/io/hdf5/hdf5_results_writer.hpp" #include "fesa/solvers/linear/mkl_pardiso_solver.h" @@ -91,8 +91,8 @@ int FesaApplication::run(const std::vector& arguments) { } AnalysisRequest request; - request.inputPath = arguments[0U]; - request.outputPath = explicitOutputForm + request.input_path = arguments[0U]; + request.output_path = explicitOutputForm ? std::filesystem::path{arguments[2U]} : std::filesystem::current_path() / "results.h5"; @@ -101,7 +101,7 @@ int FesaApplication::run(const std::vector& arguments) { Hdf5ResultsWriter resultsWriter; LinearStaticAnalysis analysis{ parallelFor, linearSolver, resultsWriter}; - const Status status = analysis.run(request); + const Status status = analysis.Run(request); if (status.IsOk()) { return kSuccessExitCode; } diff --git a/src/fesa/assembly/load_assembler.cpp b/src/fesa/assembly/load_assembler.cpp index f460c01..6dfb0a4 100644 --- a/src/fesa/assembly/load_assembler.cpp +++ b/src/fesa/assembly/load_assembler.cpp @@ -1,6 +1,4 @@ -#include "fesa/assembly/load_assembler.hpp" - -#include "fesa/constraints/essential_constraints.hpp" +#include "fesa/assembly/load_assembler.h" #include #include @@ -13,475 +11,400 @@ #include #include +#include "fesa/constraints/essential_constraints.h" + namespace fesa { namespace { -constexpr std::size_t dofsPerNode = 6U; -constexpr double shellMomentProjectionTolerance = 1.0e-12; +constexpr std::size_t kDofsPerNode = 6U; +constexpr double kShellMomentProjectionTolerance = 1.0e-12; -Status loadFailure( - const std::string& code, - const SourceLocation& location, - const std::string& keyword, - const std::string& identity, - const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, code, location, keyword, identity, message}}); +Status LoadFailure(const std::string& code, const SourceLocation& location, + const std::string& keyword, const std::string& identity, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, location, keyword, identity, message}}); } -char asciiLower(const char value) { - if (value >= 'A' && value <= 'Z') { - return static_cast(value + ('a' - 'A')); - } - return value; +char AsciiLower(const char value) { + if (value >= 'A' && value <= 'Z') { + return static_cast(value + ('a' - 'A')); + } + return value; } -bool equalName(const std::string& left, const std::string& right) { - return left.size() == right.size() && - std::equal( - left.begin(), - left.end(), - right.begin(), - [](const char leftValue, const char rightValue) { - return asciiLower(leftValue) == asciiLower(rightValue); - }); +bool EqualName(const std::string& left, const std::string& right) { + return left.size() == right.size() && + std::equal(left.begin(), left.end(), right.begin(), + [](const char left_value, const char right_value) { + return AsciiLower(left_value) == AsciiLower(right_value); + }); } -bool tryPositiveInteger(const std::string& text, std::int64_t& value) { - const char* const first = text.data(); - const char* const last = first + text.size(); - const auto parsed = std::from_chars(first, last, value); - return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; +bool TryPositiveInteger(const std::string& text, std::int64_t& value) { + const char* const first = text.data(); + const char* const last = first + text.size(); + const auto parsed = std::from_chars(first, last, value); + return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; } -bool isStrictlyIncreasing(const std::vector& values) { - return std::adjacent_find( - values.begin(), - values.end(), - [](const std::size_t left, const std::size_t right) { - return left >= right; - }) == values.end(); +/// @brief Checks that equation-space indices preserve stable full-DOF order. +bool IsStrictlyIncreasing(const std::vector& values) { + return std::adjacent_find( + values.begin(), values.end(), + [](const std::size_t left, const std::size_t right) { + return left >= right; + }) == values.end(); } -Status validateDofOrder( - const DofManager& dofs, - const std::size_t expectedFullCount, - const SourceLocation& location) { - const std::size_t fullCount = dofs.fullDofCount(); - const auto& freeDofs = dofs.freeDofs(); - const auto& constrainedDofs = dofs.constrainedDofs(); - if (fullCount != expectedFullCount || - freeDofs.size() != dofs.freeDofCount() || - constrainedDofs.size() != dofs.constrainedDofCount() || - dofs.prescribedValues().Size() != constrainedDofs.size() || - constrainedDofs.size() > fullCount || - freeDofs.size() != fullCount - constrainedDofs.size()) { - return loadFailure( - "invalid-load-dimensions", - location, - "LOAD_ASSEMBLER", - std::to_string(fullCount), - "Full, free, constrained, prescribed, and model dimensions must agree."); - } - if (!isStrictlyIncreasing(freeDofs) || - !isStrictlyIncreasing(constrainedDofs)) { - return loadFailure( - "invalid-load-order", - location, - "LOAD_ASSEMBLER", - std::to_string(fullCount), - "Free and constrained DOFs must use stable increasing full-DOF order."); - } +/// @brief Validates the full/free/constrained partition used by load assembly. +Status ValidateDofOrder(const DofManager& dofs, + const std::size_t expected_full_count, + const SourceLocation& location) { + const std::size_t full_count = dofs.FullDofCount(); + const auto& free_dofs = dofs.FreeDofs(); + const auto& constrained_dofs = dofs.ConstrainedDofs(); + if (full_count != expected_full_count || + free_dofs.size() != dofs.FreeDofCount() || + constrained_dofs.size() != dofs.ConstrainedDofCount() || + dofs.PrescribedValues().Size() != constrained_dofs.size() || + constrained_dofs.size() > full_count || + free_dofs.size() != full_count - constrained_dofs.size()) { + return LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER", + std::to_string(full_count), + "Full, free, constrained, prescribed, and model " + "dimensions must agree."); + } + if (!IsStrictlyIncreasing(free_dofs) || + !IsStrictlyIncreasing(constrained_dofs)) { + return LoadFailure( + "invalid-load-order", location, "LOAD_ASSEMBLER", + std::to_string(full_count), + "Free and constrained DOFs must use stable increasing full-DOF order."); + } - std::vector ownership(fullCount, 0U); - try { - for (std::size_t equation = 0U; - equation < freeDofs.size(); - ++equation) { - const std::size_t fullDof = freeDofs[equation]; - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof) != equation) { - return loadFailure( - "invalid-load-order", - location, - "LOAD_ASSEMBLER", - std::to_string(fullDof), - "Free equation numbering must match stable full-DOF order."); - } - ownership[fullDof] = 1U; - } - for (const std::size_t fullDof : constrainedDofs) { - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof).has_value()) { - return loadFailure( - "invalid-load-order", - location, - "LOAD_ASSEMBLER", - std::to_string(fullDof), - "Constrained DOFs must be unique and absent from free equations."); - } - ownership[fullDof] = 2U; - } - } catch (const std::out_of_range&) { - return loadFailure( - "invalid-load-dimensions", - location, - "LOAD_ASSEMBLER", - std::to_string(fullCount), - "DofManager equation storage must cover every full DOF."); + std::vector ownership(full_count, 0U); + try { + for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) { + const std::size_t full_dof = free_dofs[equation]; + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof) != equation) { + return LoadFailure( + "invalid-load-order", location, "LOAD_ASSEMBLER", + std::to_string(full_dof), + "Free equation numbering must match stable full-DOF order."); + } + ownership[full_dof] = 1U; } - if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { - return loadFailure( - "invalid-load-order", - location, - "LOAD_ASSEMBLER", - std::to_string(fullCount), - "Free and constrained DOFs must partition the full range."); + for (const std::size_t full_dof : constrained_dofs) { + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof).has_value()) { + return LoadFailure( + "invalid-load-order", location, "LOAD_ASSEMBLER", + std::to_string(full_dof), + "Constrained DOFs must be unique and absent from free equations."); + } + ownership[full_dof] = 2U; } + } catch (const std::out_of_range&) { + return LoadFailure( + "invalid-load-dimensions", location, "LOAD_ASSEMBLER", + std::to_string(full_count), + "DofManager equation storage must cover every full DOF."); + } + if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { + return LoadFailure( + "invalid-load-order", location, "LOAD_ASSEMBLER", + std::to_string(full_count), + "Free and constrained DOFs must partition the full range."); + } + return Status::Ok(); +} + +Result> ResolveTarget(const Domain& domain, + const NodalLoad& load) { + std::vector matching_sets; + for (const auto& set : domain.NodeSets()) { + if (EqualName(set.name, load.target)) { + matching_sets.push_back(&set); + } + } + + std::vector matching_nodes; + std::int64_t label = 0; + if (TryPositiveInteger(load.target, label)) { + for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { + if (domain.Nodes()[index].source_id.source_label == label) { + matching_nodes.push_back(static_cast(index)); + } + } + } + + if (matching_sets.size() > 1U || matching_nodes.size() > 1U || + (!matching_sets.empty() && !matching_nodes.empty())) { + return Result>::Failure( + LoadFailure("invalid-load-target", load.location, "CLOAD", load.target, + "The load target must resolve unambiguously to one node or " + "one expanded node set.")); + } + if (!matching_sets.empty()) { + const auto& nodes = matching_sets.front()->node_indices; + std::vector seen(domain.Nodes().size(), 0U); + for (const EntityIndex node : nodes) { + if (node >= domain.Nodes().size() || seen[node] != 0U) { + return Result>::Failure(LoadFailure( + "invalid-load-target", load.location, "CLOAD", load.target, + "The expanded node set must contain unique in-range stable node " + "identities.")); + } + seen[node] = 1U; + } + return Result>::Success(nodes); + } + if (!matching_nodes.empty()) { + return Result>::Success(std::move(matching_nodes)); + } + return Result>::Failure(LoadFailure( + "invalid-load-target", load.location, "CLOAD", load.target, + "The load target must resolve to one semantic node or node set.")); +} + +Status ValidateFiniteVector(const Vector& values, + const SourceLocation& location, + const std::string& identity) { + for (std::size_t index = 0U; index < values.Size(); ++index) { + if (!std::isfinite(values[index])) { + return LoadFailure("nonfinite-load-value", location, "LOAD_ASSEMBLER", + identity + ":" + std::to_string(index), + "Load and prescribed displacement vectors must " + "contain finite values."); + } + } + return Status::Ok(); +} + +Status ValidateShellMoments(const Domain& domain, const Vector& full_load) { + if (domain.ShellElements().empty()) { return Status::Ok(); + } + + std::vector frame_by_node(domain.Nodes().size(), + nullptr); + for (const auto& frame : domain.ShellNodeInitialFrames()) { + if (frame.node_index >= frame_by_node.size() || + frame_by_node[frame.node_index] != nullptr) { + return LoadFailure( + "invalid-shell-director", {domain.SourcePath(), 0U}, "NODE", + std::to_string(frame.node_index), + "Shell nodal directors must have unique in-range node identities."); + } + frame_by_node[frame.node_index] = &frame; + } + + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + const double moment_x = full_load[node * kDofsPerNode + 3U]; + const double moment_y = full_load[node * kDofsPerNode + 4U]; + const double moment_z = full_load[node * kDofsPerNode + 5U]; + if (moment_x == 0.0 && moment_y == 0.0 && moment_z == 0.0) { + continue; + } + + const auto* const frame = frame_by_node[node]; + if (frame == nullptr) { + return LoadFailure( + "invalid-shell-director", domain.Nodes()[node].location, "NODE", + domain.Nodes()[node].source_id.source_label_text, + "A loaded shell node must have an approved initial director."); + } + + const double moment_scale = std::max( + std::abs(moment_x), std::max(std::abs(moment_y), std::abs(moment_z))); + const double scaled_x = moment_x / moment_scale; + const double scaled_y = moment_y / moment_scale; + const double scaled_z = moment_z / moment_scale; + const double scaled_norm = std::hypot(scaled_x, scaled_y, scaled_z); + const double scaled_dot = frame->director[0U] * scaled_x + + frame->director[1U] * scaled_y + + frame->director[2U] * scaled_z; + const double projection_ratio = std::abs(scaled_dot) / scaled_norm; + if (!(projection_ratio <= kShellMomentProjectionTolerance)) { + return LoadFailure("unsupported-drilling-load", + domain.Nodes()[node].location, "CLOAD", + domain.Nodes()[node].source_id.source_label_text, + "The aggregate nodal moment has an unsupported " + "director-parallel component."); + } + } + return Status::Ok(); } -Result> resolveTarget( - const Domain& domain, - const NodalLoad& load) { - std::vector matchingSets; - for (const auto& set : domain.NodeSets()) { - if (equalName(set.name, load.target)) { - matchingSets.push_back(&set); +} // namespace + +Result LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model, + const DofManager& dofs) { + const Domain& domain = model.GetDomain(); + if (domain.Nodes().size() > + (std::numeric_limits::max)() / kDofsPerNode) { + return Result::Failure(LoadFailure( + "invalid-load-dimensions", {domain.SourcePath(), 0U}, "LOAD_ASSEMBLER", + domain.SourceContentIdentity(), + "The semantic node count cannot be represented in full-DOF order.")); + } + const std::size_t expected_full_count = domain.Nodes().size() * kDofsPerNode; + const Status dof_status = + ValidateDofOrder(dofs, expected_full_count, {domain.SourcePath(), 0U}); + if (!dof_status.IsOk()) { + return Result::Failure(dof_status); + } + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + for (std::size_t component = 0U; component < kDofsPerNode; ++component) { + try { + if (dofs.FullDof(static_cast(node), + static_cast(component)) != + node * kDofsPerNode + component) { + return Result::Failure(LoadFailure( + "invalid-load-order", domain.Nodes()[node].location, + "LOAD_ASSEMBLER", + domain.Nodes()[node].source_id.source_label_text, + "DofManager node/component identity must match full-DOF order.")); } + } catch (const std::out_of_range&) { + return Result::Failure(LoadFailure( + "invalid-load-dimensions", domain.Nodes()[node].location, + "LOAD_ASSEMBLER", domain.Nodes()[node].source_id.source_label_text, + "DofManager must provide all six DOFs for every semantic node.")); + } + } + } + + const auto& active_loads = model.ActiveLoads(); + const auto& loads = model.Step().loads; + if (active_loads.size() != loads.size()) { + return Result::Failure(LoadFailure( + "invalid-load-order", model.Step().location, "CLOAD", model.Step().name, + "The active load view must include every sole-step load once.")); + } + + Vector full_load{expected_full_count}; + // Active load indices are required to be the original source order; this + // loop is therefore also the fixed floating-point accumulation order. + for (std::size_t source_order = 0U; source_order < active_loads.size(); + ++source_order) { + const EntityIndex load_index = active_loads[source_order]; + if (static_cast(load_index) != source_order || + load_index >= loads.size()) { + return Result::Failure(LoadFailure( + "invalid-load-order", model.Step().location, "CLOAD", + std::to_string(source_order), + "Active loads must retain complete stable source order.")); + } + const auto& load = loads[load_index]; + if (load.dof < 1 || load.dof > static_cast(kDofsPerNode)) { + return Result::Failure(LoadFailure( + "invalid-load-dof", load.location, "CLOAD", load.target, + "A nodal load component must be in the range 1 through 6.")); + } + if (!std::isfinite(load.magnitude)) { + return Result::Failure( + LoadFailure("nonfinite-load-value", load.location, "CLOAD", + load.target, "A nodal load magnitude must be finite.")); } - std::vector matchingNodes; - std::int64_t label = 0; - if (tryPositiveInteger(load.target, label)) { - for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) { - if (domain.Nodes()[index].source_id.source_label == label) { - matchingNodes.push_back(static_cast(index)); - } - } + auto target = ResolveTarget(domain, load); + if (!target.HasValue()) { + return Result::Failure(target.GetStatus()); } - - if (matchingSets.size() > 1U || matchingNodes.size() > 1U || - (!matchingSets.empty() && !matchingNodes.empty())) { - return Result>::Failure(loadFailure( - "invalid-load-target", - load.location, - "CLOAD", - load.target, - "The load target must resolve unambiguously to one node or one expanded node set.")); + const auto component = static_cast(load.dof - 1); + for (const EntityIndex node : target.Value()) { + const std::size_t full_dof = dofs.FullDof(node, component); + const double accumulated = full_load[full_dof] + load.magnitude; + if (!std::isfinite(accumulated)) { + return Result::Failure(LoadFailure( + "nonfinite-load-accumulation", load.location, "CLOAD", load.target, + "Source-order load accumulation produced a nonfinite value.")); + } + full_load[full_dof] = accumulated; } - if (!matchingSets.empty()) { - const auto& nodes = matchingSets.front()->node_indices; - std::vector seen(domain.Nodes().size(), 0U); - for (const EntityIndex node : nodes) { - if (node >= domain.Nodes().size() || seen[node] != 0U) { - return Result>::Failure(loadFailure( - "invalid-load-target", - load.location, - "CLOAD", - load.target, - "The expanded node set must contain unique in-range stable node identities.")); - } - seen[node] = 1U; - } - return Result>::Success(nodes); - } - if (!matchingNodes.empty()) { - return Result>::Success( - std::move(matchingNodes)); - } - return Result>::Failure(loadFailure( - "invalid-load-target", - load.location, - "CLOAD", - load.target, - "The load target must resolve to one semantic node or node set.")); + } + const Status shell_moment_status = ValidateShellMoments(domain, full_load); + if (!shell_moment_status.IsOk()) { + return Result::Failure(shell_moment_status); + } + return Result::Success(std::move(full_load)); } -Status validateFiniteVector( - const Vector& values, - const SourceLocation& location, - const std::string& identity) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return loadFailure( - "nonfinite-load-value", - location, - "LOAD_ASSEMBLER", - identity + ":" + std::to_string(index), - "Load and prescribed displacement vectors must contain finite values."); - } +Result LoadAssembler::EffectiveFreeRhs(const Vector& full_load, + const SparseMatrix& kfc, + const Vector& prescribed_values, + const DofManager& dofs) { + const SourceLocation location{{}, 0U}; + const Status dof_status = ValidateDofOrder(dofs, full_load.Size(), location); + if (!dof_status.IsOk()) { + return Result::Failure(dof_status); + } + if (kfc.Rows() != dofs.FreeDofCount() || + kfc.Columns() != dofs.ConstrainedDofCount() || + prescribed_values.Size() != dofs.ConstrainedDofCount()) { + return Result::Failure(LoadFailure( + "invalid-load-dimensions", location, "LOAD_ASSEMBLER", + std::to_string(kfc.Rows()) + "x" + std::to_string(kfc.Columns()), + "Kfc rows/columns and prescribed values must match free/constrained " + "order.")); + } + const Status matrix_status = kfc.Validate(); + if (!matrix_status.IsOk()) { + return Result::Failure(matrix_status); + } + const Status load_status = + ValidateFiniteVector(full_load, location, "full-load"); + if (!load_status.IsOk()) { + return Result::Failure(load_status); + } + const Status prescribed_status = + ValidateFiniteVector(prescribed_values, location, "prescribed-values"); + if (!prescribed_status.IsOk()) { + return Result::Failure(prescribed_status); + } + + Vector correction{kfc.Rows()}; + for (std::size_t row = 0U; row < kfc.Rows(); ++row) { + double sum = 0.0; + for (std::size_t position = kfc.RowOffsets()[row]; + position < kfc.RowOffsets()[row + 1U]; ++position) { + const double product = kfc.Values()[position] * + prescribed_values[kfc.ColumnIndices()[position]]; + if (!std::isfinite(product)) { + return Result::Failure(LoadFailure( + "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Kfc times prescribed displacement produced a nonfinite product.")); + } + sum += product; + if (!std::isfinite(sum)) { + return Result::Failure(LoadFailure( + "nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Kfc times prescribed displacement produced a nonfinite row sum.")); + } } - return Status::Ok(); + correction[row] = sum; + } + + Vector rhs = EssentialConstraints::GatherFree(full_load, dofs); + // The constrained vector is already in DofManager order, so this is the + // approved elimination equation rhs = Ff - Kfc*dc without reordering dc. + for (std::size_t row = 0U; row < rhs.Size(); ++row) { + const double value = rhs[row] - correction[row]; + if (!std::isfinite(value)) { + return Result::Failure( + LoadFailure("nonfinite-load-accumulation", location, "LOAD_ASSEMBLER", + std::to_string(row), + "Effective RHS subtraction produced a nonfinite value.")); + } + rhs[row] = value; + } + return Result::Success(std::move(rhs)); } -Status validateShellMoments( - const Domain& domain, - const Vector& fullLoad) { - if (domain.ShellElements().empty()) { - return Status::Ok(); - } - - std::vector frameByNode( - domain.Nodes().size(), nullptr); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - if (frame.node_index >= frameByNode.size() || - frameByNode[frame.node_index] != nullptr) { - return loadFailure( - "invalid-shell-director", - {domain.SourcePath(), 0U}, - "NODE", - std::to_string(frame.node_index), - "Shell nodal directors must have unique in-range node identities."); - } - frameByNode[frame.node_index] = &frame; - } - - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - const double momentX = fullLoad[node * dofsPerNode + 3U]; - const double momentY = fullLoad[node * dofsPerNode + 4U]; - const double momentZ = fullLoad[node * dofsPerNode + 5U]; - if (momentX == 0.0 && momentY == 0.0 && momentZ == 0.0) { - continue; - } - - const auto* const frame = frameByNode[node]; - if (frame == nullptr) { - return loadFailure( - "invalid-shell-director", - domain.Nodes()[node].location, - "NODE", - domain.Nodes()[node].source_id.source_label_text, - "A loaded shell node must have an approved initial director."); - } - - const double momentScale = std::max( - std::abs(momentX), - std::max(std::abs(momentY), std::abs(momentZ))); - const double scaledX = momentX / momentScale; - const double scaledY = momentY / momentScale; - const double scaledZ = momentZ / momentScale; - const double scaledNorm = std::hypot(scaledX, scaledY, scaledZ); - const double scaledDot = - frame->director[0U] * scaledX + - frame->director[1U] * scaledY + - frame->director[2U] * scaledZ; - const double projectionRatio = std::abs(scaledDot) / scaledNorm; - if (!(projectionRatio <= shellMomentProjectionTolerance)) { - return loadFailure( - "unsupported-drilling-load", - domain.Nodes()[node].location, - "CLOAD", - domain.Nodes()[node].source_id.source_label_text, - "The aggregate nodal moment has an unsupported director-parallel component."); - } - } - return Status::Ok(); -} - -} // namespace - -Result LoadAssembler::assembleFullNodalLoad( - const AnalysisModel& model, - const DofManager& dofs) { - const Domain& domain = model.domain(); - if (domain.Nodes().size() > - (std::numeric_limits::max)() / dofsPerNode) { - return Result::Failure(loadFailure( - "invalid-load-dimensions", - {domain.SourcePath(), 0U}, - "LOAD_ASSEMBLER", - domain.SourceContentIdentity(), - "The semantic node count cannot be represented in full-DOF order.")); - } - const std::size_t expectedFullCount = - domain.Nodes().size() * dofsPerNode; - const Status dofStatus = validateDofOrder( - dofs, expectedFullCount, {domain.SourcePath(), 0U}); - if (!dofStatus.IsOk()) { - return Result::Failure(dofStatus); - } - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - for (std::size_t component = 0U; - component < dofsPerNode; - ++component) { - try { - if (dofs.fullDof( - static_cast(node), - static_cast(component)) != - node * dofsPerNode + component) { - return Result::Failure(loadFailure( - "invalid-load-order", - domain.Nodes()[node].location, - "LOAD_ASSEMBLER", - domain.Nodes()[node].source_id.source_label_text, - "DofManager node/component identity must match full-DOF order.")); - } - } catch (const std::out_of_range&) { - return Result::Failure(loadFailure( - "invalid-load-dimensions", - domain.Nodes()[node].location, - "LOAD_ASSEMBLER", - domain.Nodes()[node].source_id.source_label_text, - "DofManager must provide all six DOFs for every semantic node.")); - } - } - } - - const auto& activeLoads = model.activeLoads(); - const auto& loads = model.step().loads; - if (activeLoads.size() != loads.size()) { - return Result::Failure(loadFailure( - "invalid-load-order", - model.step().location, - "CLOAD", - model.step().name, - "The active load view must include every sole-step load once.")); - } - - Vector fullLoad{expectedFullCount}; - // Active load indices are required to be the original source order; this - // loop is therefore also the fixed floating-point accumulation order. - for (std::size_t sourceOrder = 0U; - sourceOrder < activeLoads.size(); - ++sourceOrder) { - const EntityIndex loadIndex = activeLoads[sourceOrder]; - if (static_cast(loadIndex) != sourceOrder || - loadIndex >= loads.size()) { - return Result::Failure(loadFailure( - "invalid-load-order", - model.step().location, - "CLOAD", - std::to_string(sourceOrder), - "Active loads must retain complete stable source order.")); - } - const auto& load = loads[loadIndex]; - if (load.dof < 1 || load.dof > static_cast(dofsPerNode)) { - return Result::Failure(loadFailure( - "invalid-load-dof", - load.location, - "CLOAD", - load.target, - "A nodal load component must be in the range 1 through 6.")); - } - if (!std::isfinite(load.magnitude)) { - return Result::Failure(loadFailure( - "nonfinite-load-value", - load.location, - "CLOAD", - load.target, - "A nodal load magnitude must be finite.")); - } - - auto target = resolveTarget(domain, load); - if (!target.HasValue()) { - return Result::Failure(target.GetStatus()); - } - const auto component = static_cast(load.dof - 1); - for (const EntityIndex node : target.Value()) { - const std::size_t fullDof = dofs.fullDof(node, component); - const double accumulated = fullLoad[fullDof] + load.magnitude; - if (!std::isfinite(accumulated)) { - return Result::Failure(loadFailure( - "nonfinite-load-accumulation", - load.location, - "CLOAD", - load.target, - "Source-order load accumulation produced a nonfinite value.")); - } - fullLoad[fullDof] = accumulated; - } - } - const Status shellMomentStatus = validateShellMoments(domain, fullLoad); - if (!shellMomentStatus.IsOk()) { - return Result::Failure(shellMomentStatus); - } - return Result::Success(std::move(fullLoad)); -} - -Result LoadAssembler::effectiveFreeRhs( - const Vector& fullLoad, - const SparseMatrix& kfc, - const Vector& prescribedValues, - const DofManager& dofs) { - const SourceLocation location{{}, 0U}; - const Status dofStatus = - validateDofOrder(dofs, fullLoad.Size(), location); - if (!dofStatus.IsOk()) { - return Result::Failure(dofStatus); - } - if (kfc.Rows() != dofs.freeDofCount() || - kfc.Columns() != dofs.constrainedDofCount() || - prescribedValues.Size() != dofs.constrainedDofCount()) { - return Result::Failure(loadFailure( - "invalid-load-dimensions", - location, - "LOAD_ASSEMBLER", - std::to_string(kfc.Rows()) + "x" + - std::to_string(kfc.Columns()), - "Kfc rows/columns and prescribed values must match free/constrained order.")); - } - const Status matrixStatus = kfc.Validate(); - if (!matrixStatus.IsOk()) { - return Result::Failure(matrixStatus); - } - const Status loadStatus = - validateFiniteVector(fullLoad, location, "full-load"); - if (!loadStatus.IsOk()) { - return Result::Failure(loadStatus); - } - const Status prescribedStatus = validateFiniteVector( - prescribedValues, location, "prescribed-values"); - if (!prescribedStatus.IsOk()) { - return Result::Failure(prescribedStatus); - } - - Vector correction{kfc.Rows()}; - for (std::size_t row = 0U; row < kfc.Rows(); ++row) { - double sum = 0.0; - for (std::size_t position = kfc.RowOffsets()[row]; - position < kfc.RowOffsets()[row + 1U]; - ++position) { - const double product = kfc.Values()[position] * - prescribedValues[kfc.ColumnIndices()[position]]; - if (!std::isfinite(product)) { - return Result::Failure(loadFailure( - "nonfinite-load-accumulation", - location, - "LOAD_ASSEMBLER", - std::to_string(row), - "Kfc times prescribed displacement produced a nonfinite product.")); - } - sum += product; - if (!std::isfinite(sum)) { - return Result::Failure(loadFailure( - "nonfinite-load-accumulation", - location, - "LOAD_ASSEMBLER", - std::to_string(row), - "Kfc times prescribed displacement produced a nonfinite row sum.")); - } - } - correction[row] = sum; - } - - Vector rhs = EssentialConstraints::gatherFree(fullLoad, dofs); - // The constrained vector is already in DofManager order, so this is the - // approved elimination equation rhs = Ff - Kfc*dc without reordering dc. - for (std::size_t row = 0U; row < rhs.Size(); ++row) { - const double value = rhs[row] - correction[row]; - if (!std::isfinite(value)) { - return Result::Failure(loadFailure( - "nonfinite-load-accumulation", - location, - "LOAD_ASSEMBLER", - std::to_string(row), - "Effective RHS subtraction produced a nonfinite value.")); - } - rhs[row] = value; - } - return Result::Success(std::move(rhs)); -} - -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/assembly/parallel_for.cpp b/src/fesa/assembly/parallel_for.cpp index 7f4a183..919cac7 100644 --- a/src/fesa/assembly/parallel_for.cpp +++ b/src/fesa/assembly/parallel_for.cpp @@ -1,30 +1,28 @@ -#include "fesa/assembly/parallel_for.hpp" +#include "fesa/assembly/parallel_for.h" #include namespace fesa { -void SerialParallelFor::execute( - std::size_t count, - const std::function& body) const { - for (std::size_t index = 0; index < count; ++index) { - body(index); - } +void SerialParallelFor::Execute( + std::size_t count, const std::function& body) const { + for (std::size_t index = 0; index < count; ++index) { + body(index); + } } -void TbbParallelFor::execute( - std::size_t count, - const std::function& body) const { - if (count == 0U) { - return; - } +void TbbParallelFor::Execute( + std::size_t count, const std::function& body) const { + if (count == 0U) { + return; + } - // Use oneTBB's caller-scoped scheduler policy. This adapter does not set - // process-wide concurrency or override the later MKL/TBB oversubscription - // policy. A body exception cancels sibling tasks and is rethrown; work - // already running during cancellation may still finish its indexed slot. - oneapi::tbb::parallel_for( - std::size_t{0}, count, [&body](std::size_t index) { body(index); }); + // Use oneTBB's caller-scoped scheduler policy. This adapter does not set + // process-wide concurrency or override the later MKL/TBB oversubscription + // policy. A body exception cancels sibling tasks and is rethrown; work + // already running during cancellation may still finish its indexed slot. + oneapi::tbb::parallel_for(std::size_t{0}, count, + [&body](std::size_t index) { body(index); }); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/assembly/sparse_assembler.cpp b/src/fesa/assembly/sparse_assembler.cpp index 409a8c6..b6ced7a 100644 --- a/src/fesa/assembly/sparse_assembler.cpp +++ b/src/fesa/assembly/sparse_assembler.cpp @@ -1,10 +1,4 @@ -#include "fesa/assembly/sparse_assembler.hpp" - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/assembly/parallel_for.hpp" -#include "fesa/elements/euler_beam_3d.h" -#include "fesa/elements/mitc4_shell.h" -#include "fesa/fem/dof_manager.hpp" +#include "fesa/assembly/sparse_assembler.h" #include #include @@ -14,6 +8,12 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/assembly/parallel_for.h" +#include "fesa/elements/euler_beam_3d.h" +#include "fesa/elements/mitc4_shell.h" +#include "fesa/fem/dof_manager.h" + namespace fesa { namespace { @@ -25,336 +25,277 @@ constexpr std::size_t kShellElementDofCount = 24U; constexpr std::size_t kShellContributionCount = kShellElementDofCount * kShellElementDofCount; -using BeamElementBuffer = - std::array; -using ShellElementBuffer = - std::array; +using BeamElementBuffer = std::array; +using ShellElementBuffer = std::array; -Result assemblyFailure( - const std::string& code, - const SourceLocation& location, - const std::string& identity, - const std::string& message) { - return Result::Failure(Status::Failure( - FailureCategory::kModel, - {{Severity::kError, - code, - location, - "*ELEMENT", - identity, - message}})); +Result AssemblyFailure(const std::string& code, + const SourceLocation& location, + const std::string& identity, + const std::string& message) { + return Result::Failure(Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, location, "*ELEMENT", identity, message}})); } -} // namespace +} // namespace -Result SparseAssembler::assembleStiffness( - const AnalysisModel& model, - const DofManager& dofs, - const ParallelFor& parallelFor) { - const Domain& domain = model.domain(); - if (domain.Nodes().size() > - (std::numeric_limits::max)() / kDofsPerNode || - dofs.fullDofCount() != domain.Nodes().size() * kDofsPerNode) { - return assemblyFailure( - "invalid-assembly-dimensions", - {domain.SourcePath(), 0U}, - std::to_string(dofs.fullDofCount()), - "DofManager dimensions do not match the active model nodes."); - } - if (!model.activeElements().empty() && !domain.ShellElements().empty()) { - return assemblyFailure( - "unsupported-mixed-element-model", - {domain.SourcePath(), 0U}, - "B33:FESA-MITC4", - "Sparse assembly does not support mixed beam and shell models."); +Result SparseAssembler::AssembleStiffness( + const AnalysisModel& model, const DofManager& dofs, + const ParallelFor& parallel_for) { + const Domain& domain = model.GetDomain(); + if (domain.Nodes().size() > + (std::numeric_limits::max)() / kDofsPerNode || + dofs.FullDofCount() != domain.Nodes().size() * kDofsPerNode) { + return AssemblyFailure( + "invalid-assembly-dimensions", {domain.SourcePath(), 0U}, + std::to_string(dofs.FullDofCount()), + "DofManager dimensions do not match the active model nodes."); + } + if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) { + return AssemblyFailure( + "unsupported-mixed-element-model", {domain.SourcePath(), 0U}, + "B33:FESA-MITC4", + "Sparse assembly does not support mixed beam and shell models."); + } + + if (!domain.ShellElements().empty()) { + if (domain.ShellElements().size() > + (std::numeric_limits::max)() / kShellContributionCount) { + return AssemblyFailure( + "invalid-assembly-dimensions", {domain.SourcePath(), 0U}, + std::to_string(domain.ShellElements().size()), + "Shell contribution storage exceeds the addressable range."); } - if (!domain.ShellElements().empty()) { - if (domain.ShellElements().size() > - (std::numeric_limits::max)() / - kShellContributionCount) { - return assemblyFailure( - "invalid-assembly-dimensions", - {domain.SourcePath(), 0U}, - std::to_string(domain.ShellElements().size()), - "Shell contribution storage exceeds the addressable range."); - } - - std::vector>> directorsByNode( - domain.Nodes().size()); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - if (frame.node_index >= directorsByNode.size() || - directorsByNode[frame.node_index]) { - return assemblyFailure( - "invalid-assembly-element", - {domain.SourcePath(), 0U}, - std::to_string(frame.node_index), - "Shell initial frames must map uniquely to model nodes."); - } - directorsByNode[frame.node_index] = frame.director; - } - - struct ShellInput { - std::array nodes; - std::array, 4> directors; - const ShellSection* section; - const LinearElasticMaterial* material; - std::array scatter; - }; - std::vector inputs; - inputs.reserve(domain.ShellElements().size()); - for (std::size_t elementOrder = 0U; - elementOrder < domain.ShellElements().size(); - ++elementOrder) { - const auto& element = domain.ShellElements()[elementOrder]; - if (element.material_index >= domain.Materials().size() || - element.section_index >= domain.ShellSections().size()) { - return assemblyFailure( - "invalid-assembly-element", - element.location, - element.source_id.source_label_text, - "Shell element references an entity outside the Domain."); - } - - ShellInput input{}; - input.section = &domain.ShellSections()[element.section_index]; - input.material = &domain.Materials()[element.material_index]; - try { - input.scatter = dofs.shellElementScatter( - static_cast(elementOrder)); - } catch (const std::out_of_range&) { - return assemblyFailure( - "invalid-assembly-scatter", - element.location, - element.source_id.source_label_text, - "DofManager does not contain the active shell scatter."); - } - for (std::size_t nodePosition = 0U; - nodePosition < element.node_indices.size(); - ++nodePosition) { - const EntityIndex nodeIndex = element.node_indices[nodePosition]; - if (nodeIndex >= domain.Nodes().size() || - !directorsByNode[nodeIndex]) { - return assemblyFailure( - "invalid-assembly-element", - element.location, - element.source_id.source_label_text, - "Shell element requires a valid node and initial director."); - } - input.nodes[nodePosition] = &domain.Nodes()[nodeIndex]; - input.directors[nodePosition] = *directorsByNode[nodeIndex]; - for (std::size_t component = 0U; - component < kDofsPerNode; - ++component) { - const std::size_t local = - nodePosition * kDofsPerNode + component; - const std::size_t expected = - static_cast(nodeIndex) * kDofsPerNode + - component; - if (input.scatter[local] != expected || - input.scatter[local] >= dofs.fullDofCount()) { - return assemblyFailure( - "invalid-assembly-scatter", - element.location, - element.source_id.source_label_text, - "Shell scatter does not match the active model topology."); - } - } - } - inputs.push_back(input); - } - - std::vector localBuffers(inputs.size()); - std::vector> localFailures(inputs.size()); - parallelFor.execute( - inputs.size(), - [&](const std::size_t elementOrder) { - const auto& input = inputs[elementOrder]; - const auto shell = Mitc4Shell::Create( - input.nodes, - input.directors, - *input.section, - *input.material); - if (!shell.HasValue()) { - localFailures[elementOrder] = shell.GetStatus(); - return; - } - const auto stiffness = shell.Value().Stiffness(); - if (!stiffness.HasValue()) { - localFailures[elementOrder] = stiffness.GetStatus(); - return; - } - - auto& buffer = localBuffers[elementOrder]; - for (std::size_t localRow = 0U; - localRow < kShellElementDofCount; - ++localRow) { - for (std::size_t localColumn = 0U; - localColumn < kShellElementDofCount; - ++localColumn) { - const std::size_t localOrder = - localRow * kShellElementDofCount + localColumn; - buffer[localOrder] = { - input.scatter[localRow], - input.scatter[localColumn], - stiffness.Value().stabilized_global24( - localRow, localColumn), - elementOrder, - localOrder}; - } - } - }); - - for (std::size_t elementOrder = 0U; - elementOrder < localFailures.size(); - ++elementOrder) { - if (localFailures[elementOrder]) { - return Result::Failure( - *localFailures[elementOrder]); - } - } - - std::vector contributions; - contributions.reserve( - localBuffers.size() * kShellContributionCount); - // Flatten in source-element order after workers complete. The canonical - // COO reduction remains the sole writer of global CSR values. - for (const auto& buffer : localBuffers) { - contributions.insert( - contributions.end(), buffer.begin(), buffer.end()); - } - return SparseMatrix::FromCoo( - dofs.fullDofCount(), - dofs.fullDofCount(), - std::move(contributions), - dofs.sparsePattern()); + std::vector>> directors_by_node( + domain.Nodes().size()); + for (const auto& frame : domain.ShellNodeInitialFrames()) { + if (frame.node_index >= directors_by_node.size() || + directors_by_node[frame.node_index]) { + return AssemblyFailure( + "invalid-assembly-element", {domain.SourcePath(), 0U}, + std::to_string(frame.node_index), + "Shell initial frames must map uniquely to model nodes."); + } + directors_by_node[frame.node_index] = frame.director; } - if (model.activeElements().size() > - (std::numeric_limits::max)() / - kBeamContributionCount) { - return assemblyFailure( - "invalid-assembly-dimensions", - {domain.SourcePath(), 0U}, - std::to_string(model.activeElements().size()), - "Element contribution storage exceeds the addressable range."); - } + struct ShellInput { + std::array nodes; + std::array, 4> directors; + const ShellSection* section; + const LinearElasticMaterial* material; + std::array scatter; + }; + std::vector inputs; + inputs.reserve(domain.ShellElements().size()); + for (std::size_t element_order = 0U; + element_order < domain.ShellElements().size(); ++element_order) { + const auto& element = domain.ShellElements()[element_order]; + if (element.material_index >= domain.Materials().size() || + element.section_index >= domain.ShellSections().size()) { + return AssemblyFailure( + "invalid-assembly-element", element.location, + element.source_id.source_label_text, + "Shell element references an entity outside the Domain."); + } - std::vector> scatters; - scatters.reserve(model.activeElements().size()); - for (const EntityIndex elementIndex : model.activeElements()) { - if (elementIndex >= domain.Elements().size()) { - return assemblyFailure( - "invalid-assembly-element", - {domain.SourcePath(), 0U}, - std::to_string(elementIndex), - "Active element index is outside the Domain."); + ShellInput input{}; + input.section = &domain.ShellSections()[element.section_index]; + input.material = &domain.Materials()[element.material_index]; + try { + input.scatter = + dofs.ShellElementScatter(static_cast(element_order)); + } catch (const std::out_of_range&) { + return AssemblyFailure( + "invalid-assembly-scatter", element.location, + element.source_id.source_label_text, + "DofManager does not contain the active shell scatter."); + } + for (std::size_t node_position = 0U; + node_position < element.node_indices.size(); ++node_position) { + const EntityIndex node_index = element.node_indices[node_position]; + if (node_index >= domain.Nodes().size() || + !directors_by_node[node_index]) { + return AssemblyFailure( + "invalid-assembly-element", element.location, + element.source_id.source_label_text, + "Shell element requires a valid node and initial director."); } - const auto& element = domain.Elements()[elementIndex]; - if (element.node_indices[0U] >= domain.Nodes().size() || - element.node_indices[1U] >= domain.Nodes().size() || - element.material_index >= domain.Materials().size() || - element.section_index >= domain.Sections().size()) { - return assemblyFailure( - "invalid-assembly-element", - element.location, + input.nodes[node_position] = &domain.Nodes()[node_index]; + input.directors[node_position] = *directors_by_node[node_index]; + for (std::size_t component = 0U; component < kDofsPerNode; + ++component) { + const std::size_t local = node_position * kDofsPerNode + component; + const std::size_t expected = + static_cast(node_index) * kDofsPerNode + component; + if (input.scatter[local] != expected || + input.scatter[local] >= dofs.FullDofCount()) { + return AssemblyFailure( + "invalid-assembly-scatter", element.location, element.source_id.source_label_text, - "Element references an entity outside the Domain."); + "Shell scatter does not match the active model topology."); + } } - - std::array scatter{}; - try { - scatter = dofs.elementScatter(elementIndex); - } catch (const std::out_of_range&) { - return assemblyFailure( - "invalid-assembly-scatter", - element.location, - element.source_id.source_label_text, - "DofManager does not contain the active element scatter."); - } - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - for (std::size_t component = 0U; - component < kDofsPerNode; - ++component) { - const std::size_t local = endpoint * kDofsPerNode + component; - const std::size_t expected = - static_cast(element.node_indices[endpoint]) * - kDofsPerNode + - component; - if (scatter[local] != expected || - scatter[local] >= dofs.fullDofCount()) { - return assemblyFailure( - "invalid-assembly-scatter", - element.location, - element.source_id.source_label_text, - "Element scatter does not match the active model topology."); - } - } - } - scatters.push_back(scatter); + } + inputs.push_back(input); } - std::vector localBuffers(model.activeElements().size()); - std::vector> localFailures( - model.activeElements().size()); - parallelFor.execute( - model.activeElements().size(), - [&](const std::size_t elementOrder) { - const EntityIndex elementIndex = model.activeElements()[elementOrder]; - const auto& definition = domain.Elements()[elementIndex]; - const auto beam = EulerBeam3D::Create( - domain.Nodes()[definition.node_indices[0U]], - domain.Nodes()[definition.node_indices[1U]], - domain.Sections()[definition.section_index], - domain.Materials()[definition.material_index]); - if (!beam.HasValue()) { - localFailures[elementOrder] = beam.GetStatus(); - return; - } + std::vector local_buffers(inputs.size()); + std::vector> local_failures(inputs.size()); + parallel_for.Execute(inputs.size(), [&](const std::size_t element_order) { + const auto& input = inputs[element_order]; + const auto shell = Mitc4Shell::Create(input.nodes, input.directors, + *input.section, *input.material); + if (!shell.HasValue()) { + local_failures[element_order] = shell.GetStatus(); + return; + } + const auto stiffness = shell.Value().Stiffness(); + if (!stiffness.HasValue()) { + local_failures[element_order] = stiffness.GetStatus(); + return; + } - const Matrix stiffness = beam.Value().GlobalStiffness(); - auto& buffer = localBuffers[elementOrder]; - const auto& scatter = scatters[elementOrder]; - for (std::size_t localRow = 0U; - localRow < kBeamElementDofCount; - ++localRow) { - for (std::size_t localColumn = 0U; - localColumn < kBeamElementDofCount; - ++localColumn) { - const std::size_t localOrder = - localRow * kBeamElementDofCount + localColumn; - buffer[localOrder] = { - scatter[localRow], - scatter[localColumn], - stiffness(localRow, localColumn), - elementOrder, - localOrder}; - } - } - }); - - for (std::size_t elementOrder = 0U; - elementOrder < localFailures.size(); - ++elementOrder) { - if (localFailures[elementOrder]) { - return Result::Failure( - *localFailures[elementOrder]); + auto& buffer = local_buffers[element_order]; + for (std::size_t local_row = 0U; local_row < kShellElementDofCount; + ++local_row) { + for (std::size_t local_column = 0U; + local_column < kShellElementDofCount; ++local_column) { + const std::size_t local_order = + local_row * kShellElementDofCount + local_column; + buffer[local_order] = { + input.scatter[local_row], input.scatter[local_column], + stiffness.Value().stabilized_global24(local_row, local_column), + element_order, local_order}; } + } + }); + + for (std::size_t element_order = 0U; element_order < local_failures.size(); + ++element_order) { + if (local_failures[element_order]) { + return Result::Failure(*local_failures[element_order]); + } } std::vector contributions; - contributions.reserve( - localBuffers.size() * kBeamContributionCount); - // Flatten only after all workers complete; workers never share CSR state. - for (const auto& buffer : localBuffers) { - contributions.insert( - contributions.end(), buffer.begin(), buffer.end()); + contributions.reserve(local_buffers.size() * kShellContributionCount); + // Flatten in source-element order after workers complete. The canonical + // COO reduction remains the sole writer of global CSR values. + for (const auto& buffer : local_buffers) { + contributions.insert(contributions.end(), buffer.begin(), buffer.end()); } - return SparseMatrix::FromCoo( - dofs.fullDofCount(), - dofs.fullDofCount(), - std::move(contributions), - dofs.sparsePattern()); + return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(), + std::move(contributions), + dofs.GetSparsePattern()); + } + + if (model.ActiveElements().size() > + (std::numeric_limits::max)() / kBeamContributionCount) { + return AssemblyFailure( + "invalid-assembly-dimensions", {domain.SourcePath(), 0U}, + std::to_string(model.ActiveElements().size()), + "Element contribution storage exceeds the addressable range."); + } + + std::vector> scatters; + scatters.reserve(model.ActiveElements().size()); + for (const EntityIndex element_index : model.ActiveElements()) { + if (element_index >= domain.Elements().size()) { + return AssemblyFailure("invalid-assembly-element", + {domain.SourcePath(), 0U}, + std::to_string(element_index), + "Active element index is outside the Domain."); + } + const auto& element = domain.Elements()[element_index]; + if (element.node_indices[0U] >= domain.Nodes().size() || + element.node_indices[1U] >= domain.Nodes().size() || + element.material_index >= domain.Materials().size() || + element.section_index >= domain.Sections().size()) { + return AssemblyFailure( + "invalid-assembly-element", element.location, + element.source_id.source_label_text, + "Element references an entity outside the Domain."); + } + + std::array scatter{}; + try { + scatter = dofs.ElementScatter(element_index); + } catch (const std::out_of_range&) { + return AssemblyFailure( + "invalid-assembly-scatter", element.location, + element.source_id.source_label_text, + "DofManager does not contain the active element scatter."); + } + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + for (std::size_t component = 0U; component < kDofsPerNode; ++component) { + const std::size_t local = endpoint * kDofsPerNode + component; + const std::size_t expected = + static_cast(element.node_indices[endpoint]) * + kDofsPerNode + + component; + if (scatter[local] != expected || + scatter[local] >= dofs.FullDofCount()) { + return AssemblyFailure( + "invalid-assembly-scatter", element.location, + element.source_id.source_label_text, + "Element scatter does not match the active model topology."); + } + } + } + scatters.push_back(scatter); + } + + std::vector local_buffers(model.ActiveElements().size()); + std::vector> local_failures( + model.ActiveElements().size()); + parallel_for.Execute( + model.ActiveElements().size(), [&](const std::size_t element_order) { + const EntityIndex element_index = model.ActiveElements()[element_order]; + const auto& definition = domain.Elements()[element_index]; + const auto beam = + EulerBeam3D::Create(domain.Nodes()[definition.node_indices[0U]], + domain.Nodes()[definition.node_indices[1U]], + domain.Sections()[definition.section_index], + domain.Materials()[definition.material_index]); + if (!beam.HasValue()) { + local_failures[element_order] = beam.GetStatus(); + return; + } + + const Matrix stiffness = beam.Value().GlobalStiffness(); + auto& buffer = local_buffers[element_order]; + const auto& scatter = scatters[element_order]; + for (std::size_t local_row = 0U; local_row < kBeamElementDofCount; + ++local_row) { + for (std::size_t local_column = 0U; + local_column < kBeamElementDofCount; ++local_column) { + const std::size_t local_order = + local_row * kBeamElementDofCount + local_column; + buffer[local_order] = {scatter[local_row], scatter[local_column], + stiffness(local_row, local_column), + element_order, local_order}; + } + } + }); + + for (std::size_t element_order = 0U; element_order < local_failures.size(); + ++element_order) { + if (local_failures[element_order]) { + return Result::Failure(*local_failures[element_order]); + } + } + + std::vector contributions; + contributions.reserve(local_buffers.size() * kBeamContributionCount); + // Flatten only after all workers complete; workers never share CSR state. + for (const auto& buffer : local_buffers) { + contributions.insert(contributions.end(), buffer.begin(), buffer.end()); + } + return SparseMatrix::FromCoo(dofs.FullDofCount(), dofs.FullDofCount(), + std::move(contributions), + dofs.GetSparsePattern()); } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/constraints/essential_constraints.cpp b/src/fesa/constraints/essential_constraints.cpp index a562ad9..158a89b 100644 --- a/src/fesa/constraints/essential_constraints.cpp +++ b/src/fesa/constraints/essential_constraints.cpp @@ -1,6 +1,4 @@ -#include "fesa/constraints/essential_constraints.hpp" - -#include "fesa/fem/dof_manager.hpp" +#include "fesa/constraints/essential_constraints.h" #include #include @@ -9,253 +7,220 @@ #include #include +#include "fesa/fem/dof_manager.h" + namespace fesa { namespace { -Status constraintFailure( - const std::string& code, - const std::string& identity, - const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, - code, - {{}, 0U}, - "ESSENTIAL_CONSTRAINTS", - identity, - message}}); +Status ConstraintFailure(const std::string& code, const std::string& identity, + const std::string& message) { + return Status::Failure(FailureCategory::kModel, {{Severity::kError, + code, + {{}, 0U}, + "ESSENTIAL_CONSTRAINTS", + identity, + message}}); } -bool isStrictlyIncreasing(const std::vector& values) { - return std::adjacent_find( - values.begin(), - values.end(), - [](const std::size_t left, const std::size_t right) { - return left >= right; - }) == values.end(); +bool IsStrictlyIncreasing(const std::vector& values) { + return std::adjacent_find( + values.begin(), values.end(), + [](const std::size_t left, const std::size_t right) { + return left >= right; + }) == values.end(); } -Status validateDofOrder(const DofManager& dofs) { - const std::size_t fullCount = dofs.fullDofCount(); - const auto& freeDofs = dofs.freeDofs(); - const auto& constrainedDofs = dofs.constrainedDofs(); - if (freeDofs.size() != dofs.freeDofCount() || - constrainedDofs.size() != dofs.constrainedDofCount() || - dofs.prescribedValues().Size() != constrainedDofs.size() || - constrainedDofs.size() > fullCount || - freeDofs.size() != fullCount - constrainedDofs.size()) { - return constraintFailure( - "invalid-constraint-dimensions", - std::to_string(fullCount), - "DofManager full, free, constrained, and prescribed dimensions must agree."); - } - if (!isStrictlyIncreasing(freeDofs) || - !isStrictlyIncreasing(constrainedDofs)) { - return constraintFailure( - "invalid-constraint-order", - std::to_string(fullCount), - "Free and constrained DOFs must use stable increasing full-DOF order."); - } +/// @brief Validates the stable full/free/constrained numbering invariant. +Status ValidateDofOrder(const DofManager& dofs) { + const std::size_t full_count = dofs.FullDofCount(); + const auto& free_dofs = dofs.FreeDofs(); + const auto& constrained_dofs = dofs.ConstrainedDofs(); + if (free_dofs.size() != dofs.FreeDofCount() || + constrained_dofs.size() != dofs.ConstrainedDofCount() || + dofs.PrescribedValues().Size() != constrained_dofs.size() || + constrained_dofs.size() > full_count || + free_dofs.size() != full_count - constrained_dofs.size()) { + return ConstraintFailure("invalid-constraint-dimensions", + std::to_string(full_count), + "DofManager full, free, constrained, and " + "prescribed dimensions must agree."); + } + if (!IsStrictlyIncreasing(free_dofs) || + !IsStrictlyIncreasing(constrained_dofs)) { + return ConstraintFailure( + "invalid-constraint-order", std::to_string(full_count), + "Free and constrained DOFs must use stable increasing full-DOF order."); + } - std::vector ownership(fullCount, 0U); - try { - for (std::size_t equation = 0U; - equation < freeDofs.size(); - ++equation) { - const std::size_t fullDof = freeDofs[equation]; - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof) != equation) { - return constraintFailure( - "invalid-constraint-order", - std::to_string(fullDof), - "Free equation numbering must match the stable free-DOF order."); - } - ownership[fullDof] = 1U; - } - for (const std::size_t fullDof : constrainedDofs) { - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof).has_value()) { - return constraintFailure( - "invalid-constraint-order", - std::to_string(fullDof), - "Constrained DOFs must be unique and absent from free equations."); - } - ownership[fullDof] = 2U; - } - } catch (const std::out_of_range&) { - return constraintFailure( - "invalid-constraint-dimensions", - std::to_string(fullCount), - "DofManager equation storage does not cover every full DOF."); + std::vector ownership(full_count, 0U); + try { + for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) { + const std::size_t full_dof = free_dofs[equation]; + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof) != equation) { + return ConstraintFailure( + "invalid-constraint-order", std::to_string(full_dof), + "Free equation numbering must match the stable free-DOF order."); + } + ownership[full_dof] = 1U; } - if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { - return constraintFailure( - "invalid-constraint-order", - std::to_string(fullCount), - "Free and constrained DOFs must partition the complete full-DOF range."); + for (const std::size_t full_dof : constrained_dofs) { + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof).has_value()) { + return ConstraintFailure( + "invalid-constraint-order", std::to_string(full_dof), + "Constrained DOFs must be unique and absent from free equations."); + } + ownership[full_dof] = 2U; } - return Status::Ok(); + } catch (const std::out_of_range&) { + return ConstraintFailure( + "invalid-constraint-dimensions", std::to_string(full_count), + "DofManager equation storage does not cover every full DOF."); + } + if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { + return ConstraintFailure("invalid-constraint-order", + std::to_string(full_count), + "Free and constrained DOFs must partition the " + "complete full-DOF range."); + } + return Status::Ok(); } -Result extractBlock( - const SparseMatrix& full, - const std::vector& rowDofs, - const std::vector& columnDofs) { - const std::size_t absent = (std::numeric_limits::max)(); - std::vector localColumn(full.Columns(), absent); - for (std::size_t column = 0U; column < columnDofs.size(); ++column) { - localColumn[columnDofs[column]] = column; - } +/// @brief Extracts one partition without changing the supplied DOF order. +Result ExtractBlock(const SparseMatrix& full, + const std::vector& row_dofs, + const std::vector& column_dofs) { + const std::size_t absent = (std::numeric_limits::max)(); + std::vector local_column(full.Columns(), absent); + for (std::size_t column = 0U; column < column_dofs.size(); ++column) { + local_column[column_dofs[column]] = column; + } - SparsePattern pattern; - pattern.rowOffsets.reserve(rowDofs.size() + 1U); - pattern.rowOffsets.push_back(0U); - std::vector contributions; - contributions.reserve(full.Values().size()); - for (std::size_t localRow = 0U; - localRow < rowDofs.size(); - ++localRow) { - const std::size_t fullRow = rowDofs[localRow]; - for (std::size_t position = full.RowOffsets()[fullRow]; - position < full.RowOffsets()[fullRow + 1U]; - ++position) { - const std::size_t column = - localColumn[full.ColumnIndices()[position]]; - if (column == absent) { - continue; - } - pattern.columnIndices.push_back(column); - // One source CSR entry maps to one block slot, so exact numeric - // values and structural zeros survive without a new reduction. - contributions.push_back({ - localRow, - column, - full.Values()[position], - localRow, - position}); - } - pattern.rowOffsets.push_back(pattern.columnIndices.size()); + SparsePattern pattern; + pattern.row_offsets.reserve(row_dofs.size() + 1U); + pattern.row_offsets.push_back(0U); + std::vector contributions; + contributions.reserve(full.Values().size()); + for (std::size_t local_row = 0U; local_row < row_dofs.size(); ++local_row) { + const std::size_t full_row = row_dofs[local_row]; + for (std::size_t position = full.RowOffsets()[full_row]; + position < full.RowOffsets()[full_row + 1U]; ++position) { + const std::size_t column = local_column[full.ColumnIndices()[position]]; + if (column == absent) { + continue; + } + pattern.column_indices.push_back(column); + // One source CSR entry maps to one block slot, so exact numeric + // values and structural zeros survive without a new reduction. + contributions.push_back( + {local_row, column, full.Values()[position], local_row, position}); } - return SparseMatrix::FromCoo( - rowDofs.size(), - columnDofs.size(), - std::move(contributions), - pattern); + pattern.row_offsets.push_back(pattern.column_indices.size()); + } + return SparseMatrix::FromCoo(row_dofs.size(), column_dofs.size(), + std::move(contributions), pattern); } -void requireDofOrder(const DofManager& dofs) { - if (!validateDofOrder(dofs).IsOk()) { - throw std::invalid_argument{ - "DofManager constraint dimensions or order are invalid."}; - } +void RequireDofOrder(const DofManager& dofs) { + if (!ValidateDofOrder(dofs).IsOk()) { + throw std::invalid_argument{ + "DofManager constraint dimensions or order are invalid."}; + } } -} // namespace +} // namespace -Result EssentialConstraints::partition( - const SparseMatrix& full, - const DofManager& dofs) { - const Status matrixStatus = full.Validate(); - if (!matrixStatus.IsOk()) { - return Result::Failure(matrixStatus); - } - if (full.Rows() != full.Columns() || - full.Rows() != dofs.fullDofCount()) { - return Result::Failure(constraintFailure( - "invalid-constraint-dimensions", - std::to_string(full.Rows()) + "x" + - std::to_string(full.Columns()), - "Full stiffness must be square and match the DofManager full dimension.")); - } - const Status dofStatus = validateDofOrder(dofs); - if (!dofStatus.IsOk()) { - return Result::Failure(dofStatus); - } +Result EssentialConstraints::Partition( + const SparseMatrix& full, const DofManager& dofs) { + const Status matrix_status = full.Validate(); + if (!matrix_status.IsOk()) { + return Result::Failure(matrix_status); + } + if (full.Rows() != full.Columns() || full.Rows() != dofs.FullDofCount()) { + return Result::Failure(ConstraintFailure( + "invalid-constraint-dimensions", + std::to_string(full.Rows()) + "x" + std::to_string(full.Columns()), + "Full stiffness must be square and match the DofManager full " + "dimension.")); + } + const Status dof_status = ValidateDofOrder(dofs); + if (!dof_status.IsOk()) { + return Result::Failure(dof_status); + } - auto kff = extractBlock(full, dofs.freeDofs(), dofs.freeDofs()); - if (!kff.HasValue()) { - return Result::Failure(kff.GetStatus()); - } - auto kfc = extractBlock(full, dofs.freeDofs(), dofs.constrainedDofs()); - if (!kfc.HasValue()) { - return Result::Failure(kfc.GetStatus()); - } - auto kcf = extractBlock(full, dofs.constrainedDofs(), dofs.freeDofs()); - if (!kcf.HasValue()) { - return Result::Failure(kcf.GetStatus()); - } - auto kcc = extractBlock( - full, dofs.constrainedDofs(), dofs.constrainedDofs()); - if (!kcc.HasValue()) { - return Result::Failure(kcc.GetStatus()); - } + auto kff = ExtractBlock(full, dofs.FreeDofs(), dofs.FreeDofs()); + if (!kff.HasValue()) { + return Result::Failure(kff.GetStatus()); + } + auto kfc = ExtractBlock(full, dofs.FreeDofs(), dofs.ConstrainedDofs()); + if (!kfc.HasValue()) { + return Result::Failure(kfc.GetStatus()); + } + auto kcf = ExtractBlock(full, dofs.ConstrainedDofs(), dofs.FreeDofs()); + if (!kcf.HasValue()) { + return Result::Failure(kcf.GetStatus()); + } + auto kcc = ExtractBlock(full, dofs.ConstrainedDofs(), dofs.ConstrainedDofs()); + if (!kcc.HasValue()) { + return Result::Failure(kcc.GetStatus()); + } - return Result::Success({ - std::move(kff.Value()), - std::move(kfc.Value()), - std::move(kcf.Value()), - std::move(kcc.Value())}); + return Result::Success( + {std::move(kff.Value()), std::move(kfc.Value()), std::move(kcf.Value()), + std::move(kcc.Value())}); } -Vector EssentialConstraints::gatherFree( - const Vector& full, - const DofManager& dofs) { - requireDofOrder(dofs); - if (full.Size() != dofs.fullDofCount()) { - throw std::invalid_argument{ - "Full vector size must match the DofManager full dimension."}; - } - Vector reduced{dofs.freeDofCount()}; - for (std::size_t equation = 0U; - equation < dofs.freeDofs().size(); - ++equation) { - reduced[equation] = full[dofs.freeDofs()[equation]]; - } - return reduced; +Vector EssentialConstraints::GatherFree(const Vector& full, + const DofManager& dofs) { + RequireDofOrder(dofs); + if (full.Size() != dofs.FullDofCount()) { + throw std::invalid_argument{ + "Full vector size must match the DofManager full dimension."}; + } + Vector reduced{dofs.FreeDofCount()}; + for (std::size_t equation = 0U; equation < dofs.FreeDofs().size(); + ++equation) { + reduced[equation] = full[dofs.FreeDofs()[equation]]; + } + return reduced; } -Vector EssentialConstraints::gatherConstrained( - const Vector& full, - const DofManager& dofs) { - requireDofOrder(dofs); - if (full.Size() != dofs.fullDofCount()) { - throw std::invalid_argument{ - "Full vector size must match the DofManager full dimension."}; - } - Vector reduced{dofs.constrainedDofCount()}; - for (std::size_t index = 0U; - index < dofs.constrainedDofs().size(); - ++index) { - reduced[index] = full[dofs.constrainedDofs()[index]]; - } - return reduced; +Vector EssentialConstraints::GatherConstrained(const Vector& full, + const DofManager& dofs) { + RequireDofOrder(dofs); + if (full.Size() != dofs.FullDofCount()) { + throw std::invalid_argument{ + "Full vector size must match the DofManager full dimension."}; + } + Vector reduced{dofs.ConstrainedDofCount()}; + for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size(); ++index) { + reduced[index] = full[dofs.ConstrainedDofs()[index]]; + } + return reduced; } -Vector EssentialConstraints::reconstructFull( - const Vector& freeValues, - const Vector& constrainedValues, - const DofManager& dofs) { - requireDofOrder(dofs); - if (freeValues.Size() != dofs.freeDofCount() || - constrainedValues.Size() != dofs.constrainedDofCount()) { - throw std::invalid_argument{ - "Reduced vector sizes must match the DofManager order."}; - } +Vector EssentialConstraints::ReconstructFull(const Vector& free_values, + const Vector& constrained_values, + const DofManager& dofs) { + RequireDofOrder(dofs); + if (free_values.Size() != dofs.FreeDofCount() || + constrained_values.Size() != dofs.ConstrainedDofCount()) { + throw std::invalid_argument{ + "Reduced vector sizes must match the DofManager order."}; + } - Vector full{dofs.fullDofCount()}; - for (std::size_t equation = 0U; - equation < dofs.freeDofs().size(); - ++equation) { - full[dofs.freeDofs()[equation]] = freeValues[equation]; - } - // Preserve caller-supplied dc exactly; nonzero prescribed displacement is - // never replaced with an implicit homogeneous constraint. - for (std::size_t index = 0U; - index < dofs.constrainedDofs().size(); - ++index) { - full[dofs.constrainedDofs()[index]] = constrainedValues[index]; - } - return full; + Vector full{dofs.FullDofCount()}; + for (std::size_t equation = 0U; equation < dofs.FreeDofs().size(); + ++equation) { + full[dofs.FreeDofs()[equation]] = free_values[equation]; + } + // Preserve caller-supplied dc exactly; nonzero prescribed displacement is + // never replaced with an implicit homogeneous constraint. + for (std::size_t index = 0U; index < dofs.ConstrainedDofs().size(); ++index) { + full[dofs.ConstrainedDofs()[index]] = constrained_values[index]; + } + return full; } -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/fem/dof_manager.cpp b/src/fesa/fem/dof_manager.cpp index 68a8814..3c39038 100644 --- a/src/fesa/fem/dof_manager.cpp +++ b/src/fesa/fem/dof_manager.cpp @@ -1,4 +1,4 @@ -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include #include @@ -10,263 +10,248 @@ namespace fesa { namespace { -constexpr std::size_t dofsPerNode = 6U; +constexpr std::size_t kDofsPerNode = 6U; -char asciiLower(char value) { - if (value >= 'A' && value <= 'Z') { - return static_cast(value + ('a' - 'A')); - } - return value; +char AsciiLower(char value) { + if (value >= 'A' && value <= 'Z') { + return static_cast(value + ('a' - 'A')); + } + return value; } -bool equalName(const std::string& left, const std::string& right) { - return left.size() == right.size() && - std::equal( - left.begin(), left.end(), right.begin(), - [](char leftValue, char rightValue) { - return asciiLower(leftValue) == asciiLower(rightValue); - }); +bool EqualName(const std::string& left, const std::string& right) { + return left.size() == right.size() && + std::equal(left.begin(), left.end(), right.begin(), + [](char left_value, char right_value) { + return AsciiLower(left_value) == AsciiLower(right_value); + }); } -bool tryPositiveInteger(const std::string& text, std::int64_t& value) { - const char* const first = text.data(); - const char* const last = first + text.size(); - const auto parsed = std::from_chars(first, last, value); - return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; +bool TryPositiveInteger(const std::string& text, std::int64_t& value) { + const char* const first = text.data(); + const char* const last = first + text.size(); + const auto parsed = std::from_chars(first, last, value); + return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; } -std::vector expandBoundaryTarget( +std::vector ExpandBoundaryTarget( const Domain& domain, const BoundaryCondition& boundary) { - for (const auto& set : domain.NodeSets()) { - if (equalName(set.name, boundary.target)) { - return set.node_indices; + for (const auto& set : domain.NodeSets()) { + if (EqualName(set.name, boundary.target)) { + return set.node_indices; + } + } + + std::int64_t source_label = 0; + if (TryPositiveInteger(boundary.target, source_label)) { + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + if (domain.Nodes()[node].source_id.source_label == source_label) { + return {static_cast(node)}; + } + } + } + return {}; +} + +template +void AppendScatter(std::vector>& columns_by_row, + const std::array& scatter) { + for (const std::size_t row : scatter) { + auto& columns = columns_by_row[row]; + columns.insert(columns.end(), scatter.begin(), scatter.end()); + } +} + +/// @brief Builds sorted unique CSR columns by deterministic scatter traversal. +SparsePattern BuildSparsePattern( + std::size_t full_dof_count, const std::vector& active_elements, + const std::vector>& element_scatters, + const std::vector>& shell_element_scatters) { + std::vector> columns_by_row(full_dof_count); + for (const EntityIndex element : active_elements) { + AppendScatter(columns_by_row, element_scatters.at(element)); + } + // Every shell in the approved single-step shell subset is active. + for (const auto& scatter : shell_element_scatters) { + AppendScatter(columns_by_row, scatter); + } + + SparsePattern pattern; + pattern.row_offsets.reserve(full_dof_count + 1U); + pattern.row_offsets.push_back(0U); + for (auto& columns : columns_by_row) { + // Stable CSR structure is independent of element traversal duplicates. + std::sort(columns.begin(), columns.end()); + columns.erase(std::unique(columns.begin(), columns.end()), columns.end()); + pattern.column_indices.insert(pattern.column_indices.end(), columns.begin(), + columns.end()); + pattern.row_offsets.push_back(pattern.column_indices.size()); + } + return pattern; +} + +} // namespace + +Result DofManager::Create(const AnalysisModel& model) { + const Domain& domain = model.GetDomain(); + const std::size_t full_count = domain.Nodes().size() * kDofsPerNode; + + std::vector> prescribed_by_full_dof(full_count); + for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) { + const auto& boundary = model.Step().boundaries.at(boundary_index); + const auto target = ExpandBoundaryTarget(domain, boundary); + for (const EntityIndex node : target) { + for (int component = boundary.first_dof; component <= boundary.last_dof; + ++component) { + const std::size_t full_dof = + static_cast(node) * kDofsPerNode + + static_cast(component - 1); + auto& prescribed = prescribed_by_full_dof[full_dof]; + if (prescribed && *prescribed != boundary.value) { + return Result::Failure(Status::Failure( + FailureCategory::kInput, + {{Severity::kError, "conflicting-boundary-condition", + boundary.location, "BOUNDARY", boundary.target, + "Expanded boundary rows prescribe different values to one " + "node/DOF."}})); } + prescribed = boundary.value; + } } + } - std::int64_t sourceLabel = 0; - if (tryPositiveInteger(boundary.target, sourceLabel)) { - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - if (domain.Nodes()[node].source_id.source_label == sourceLabel) { - return {static_cast(node)}; - } - } + std::vector free_dofs; + std::vector constrained_dofs; + std::vector constrained_values; + std::vector> free_equations(full_count); + free_dofs.reserve(full_count); + constrained_dofs.reserve(full_count); + constrained_values.reserve(full_count); + // A full-DOF scan fixes free equations, constrained DOFs, and dc in the + // same stable order regardless of boundary declaration overlap. + for (std::size_t full_dof = 0U; full_dof < full_count; ++full_dof) { + if (prescribed_by_full_dof[full_dof]) { + constrained_dofs.push_back(full_dof); + constrained_values.push_back(*prescribed_by_full_dof[full_dof]); + } else { + free_equations[full_dof] = free_dofs.size(); + free_dofs.push_back(full_dof); } - return {}; + } + + Vector prescribed_values{constrained_values.size()}; + for (std::size_t index = 0U; index < constrained_values.size(); ++index) { + prescribed_values[index] = constrained_values[index]; + } + + std::vector> element_scatters( + domain.Elements().size()); + for (const EntityIndex element_index : model.ActiveElements()) { + const auto& element = domain.Elements().at(element_index); + auto& scatter = element_scatters.at(element_index); + for (std::size_t endpoint = 0U; endpoint < element.node_indices.size(); + ++endpoint) { + const std::size_t node = element.node_indices[endpoint]; + for (std::size_t component = 0U; component < kDofsPerNode; ++component) { + scatter[endpoint * kDofsPerNode + component] = + node * kDofsPerNode + component; + } + } + } + + std::vector> shell_element_scatters( + domain.ShellElements().size()); + for (std::size_t element_index = 0U; + element_index < domain.ShellElements().size(); ++element_index) { + const auto& element = domain.ShellElements()[element_index]; + auto& scatter = shell_element_scatters[element_index]; + for (std::size_t node_position = 0U; + node_position < element.node_indices.size(); ++node_position) { + const std::size_t node = element.node_indices[node_position]; + for (std::size_t component = 0U; component < kDofsPerNode; ++component) { + scatter[node_position * kDofsPerNode + component] = + node * kDofsPerNode + component; + } + } + } + + auto pattern = BuildSparsePattern(full_count, model.ActiveElements(), + element_scatters, shell_element_scatters); + return Result::Success( + DofManager{full_count, std::move(free_equations), + std::move(element_scatters), std::move(shell_element_scatters), + std::move(free_dofs), std::move(constrained_dofs), + std::move(prescribed_values), std::move(pattern)}); } -template -void appendScatter( - std::vector>& columnsByRow, - const std::array& scatter) { - for (const std::size_t row : scatter) { - auto& columns = columnsByRow[row]; - columns.insert(columns.end(), scatter.begin(), scatter.end()); - } +std::size_t DofManager::FullDofCount() const noexcept { + return full_dof_count_; } -SparsePattern buildSparsePattern( - std::size_t fullDofCount, - const std::vector& activeElements, - const std::vector>& elementScatters, - const std::vector>& shellElementScatters) { - std::vector> columnsByRow(fullDofCount); - for (const EntityIndex element : activeElements) { - appendScatter(columnsByRow, elementScatters.at(element)); - } - // Every shell in the approved single-step shell subset is active. - for (const auto& scatter : shellElementScatters) { - appendScatter(columnsByRow, scatter); - } - - SparsePattern pattern; - pattern.rowOffsets.reserve(fullDofCount + 1U); - pattern.rowOffsets.push_back(0U); - for (auto& columns : columnsByRow) { - // Stable CSR structure is independent of element traversal duplicates. - std::sort(columns.begin(), columns.end()); - columns.erase(std::unique(columns.begin(), columns.end()), columns.end()); - pattern.columnIndices.insert( - pattern.columnIndices.end(), columns.begin(), columns.end()); - pattern.rowOffsets.push_back(pattern.columnIndices.size()); - } - return pattern; +std::size_t DofManager::FreeDofCount() const noexcept { + return free_dofs_.size(); } -} // namespace - -Result DofManager::create(const AnalysisModel& model) { - const Domain& domain = model.domain(); - const std::size_t fullCount = domain.Nodes().size() * dofsPerNode; - - std::vector> prescribedByFullDof(fullCount); - for (const EntityIndex boundaryIndex : model.activeBoundaryConditions()) { - const auto& boundary = model.step().boundaries.at(boundaryIndex); - const auto target = expandBoundaryTarget(domain, boundary); - for (const EntityIndex node : target) { - for (int component = boundary.first_dof; - component <= boundary.last_dof; - ++component) { - const std::size_t fullDof = - static_cast(node) * dofsPerNode + - static_cast(component - 1); - auto& prescribed = prescribedByFullDof[fullDof]; - if (prescribed && *prescribed != boundary.value) { - return Result::Failure(Status::Failure( - FailureCategory::kInput, - {{Severity::kError, - "conflicting-boundary-condition", - boundary.location, - "BOUNDARY", - boundary.target, - "Expanded boundary rows prescribe different values to one node/DOF."}})); - } - prescribed = boundary.value; - } - } - } - - std::vector freeDofs; - std::vector constrainedDofs; - std::vector constrainedValues; - std::vector> freeEquations(fullCount); - freeDofs.reserve(fullCount); - constrainedDofs.reserve(fullCount); - constrainedValues.reserve(fullCount); - // A full-DOF scan fixes free equations, constrained DOFs, and dc in the - // same stable order regardless of boundary declaration overlap. - for (std::size_t fullDof = 0U; fullDof < fullCount; ++fullDof) { - if (prescribedByFullDof[fullDof]) { - constrainedDofs.push_back(fullDof); - constrainedValues.push_back(*prescribedByFullDof[fullDof]); - } else { - freeEquations[fullDof] = freeDofs.size(); - freeDofs.push_back(fullDof); - } - } - - Vector prescribedValues{constrainedValues.size()}; - for (std::size_t index = 0U; index < constrainedValues.size(); ++index) { - prescribedValues[index] = constrainedValues[index]; - } - - std::vector> elementScatters( - domain.Elements().size()); - for (const EntityIndex elementIndex : model.activeElements()) { - const auto& element = domain.Elements().at(elementIndex); - auto& scatter = elementScatters.at(elementIndex); - for (std::size_t endpoint = 0U; endpoint < element.node_indices.size(); ++endpoint) { - const std::size_t node = element.node_indices[endpoint]; - for (std::size_t component = 0U; component < dofsPerNode; ++component) { - scatter[endpoint * dofsPerNode + component] = - node * dofsPerNode + component; - } - } - } - - std::vector> shellElementScatters( - domain.ShellElements().size()); - for (std::size_t elementIndex = 0U; - elementIndex < domain.ShellElements().size(); - ++elementIndex) { - const auto& element = domain.ShellElements()[elementIndex]; - auto& scatter = shellElementScatters[elementIndex]; - for (std::size_t nodePosition = 0U; - nodePosition < element.node_indices.size(); - ++nodePosition) { - const std::size_t node = element.node_indices[nodePosition]; - for (std::size_t component = 0U; - component < dofsPerNode; - ++component) { - scatter[nodePosition * dofsPerNode + component] = - node * dofsPerNode + component; - } - } - } - - auto pattern = buildSparsePattern( - fullCount, - model.activeElements(), - elementScatters, - shellElementScatters); - return Result::Success(DofManager{ - fullCount, - std::move(freeEquations), - std::move(elementScatters), - std::move(shellElementScatters), - std::move(freeDofs), - std::move(constrainedDofs), - std::move(prescribedValues), - std::move(pattern)}); +std::size_t DofManager::ConstrainedDofCount() const noexcept { + return constrained_dofs_.size(); } -std::size_t DofManager::fullDofCount() const noexcept { - return fullDofCount_; +std::size_t DofManager::FullDof(EntityIndex node, + DofComponent component) const { + const std::size_t component_index = static_cast(component); + if (node >= full_dof_count_ / kDofsPerNode || + component_index >= kDofsPerNode) { + throw std::out_of_range{"Node or DOF component is out of range."}; + } + return static_cast(node) * kDofsPerNode + component_index; } -std::size_t DofManager::freeDofCount() const noexcept { - return freeDofs_.size(); +std::optional DofManager::FreeEquation( + std::size_t full_dof) const { + return free_equations_.at(full_dof); } -std::size_t DofManager::constrainedDofCount() const noexcept { - return constrainedDofs_.size(); -} - -std::size_t DofManager::fullDof( - EntityIndex node, DofComponent component) const { - const std::size_t componentIndex = static_cast(component); - if (node >= fullDofCount_ / dofsPerNode || componentIndex >= dofsPerNode) { - throw std::out_of_range{"Node or DOF component is out of range."}; - } - return static_cast(node) * dofsPerNode + componentIndex; -} - -std::optional DofManager::freeEquation( - std::size_t fullDof) const { - return freeEquations_.at(fullDof); -} - -const std::array& DofManager::elementScatter( +const std::array& DofManager::ElementScatter( EntityIndex element) const { - return elementScatters_.at(element); + return element_scatters_.at(element); } -const std::array& DofManager::shellElementScatter( +const std::array& DofManager::ShellElementScatter( EntityIndex element) const { - return shellElementScatters_.at(element); + return shell_element_scatters_.at(element); } -const std::vector& DofManager::freeDofs() const noexcept { - return freeDofs_; +const std::vector& DofManager::FreeDofs() const noexcept { + return free_dofs_; } -const std::vector& DofManager::constrainedDofs() const noexcept { - return constrainedDofs_; +const std::vector& DofManager::ConstrainedDofs() const noexcept { + return constrained_dofs_; } -const Vector& DofManager::prescribedValues() const noexcept { - return prescribedValues_; +const Vector& DofManager::PrescribedValues() const noexcept { + return prescribed_values_; } -const SparsePattern& DofManager::sparsePattern() const noexcept { - return sparsePattern_; +const SparsePattern& DofManager::GetSparsePattern() const noexcept { + return sparse_pattern_; } DofManager::DofManager( - std::size_t fullDofCount, - std::vector> freeEquations, - std::vector> elementScatters, - std::vector> shellElementScatters, - std::vector freeDofs, - std::vector constrainedDofs, - Vector prescribedValues, - SparsePattern sparsePattern) - : fullDofCount_{fullDofCount}, - freeEquations_{std::move(freeEquations)}, - elementScatters_{std::move(elementScatters)}, - shellElementScatters_{std::move(shellElementScatters)}, - freeDofs_{std::move(freeDofs)}, - constrainedDofs_{std::move(constrainedDofs)}, - prescribedValues_{std::move(prescribedValues)}, - sparsePattern_{std::move(sparsePattern)} {} + std::size_t full_dof_count, + std::vector> free_equations, + std::vector> element_scatters, + std::vector> shell_element_scatters, + std::vector free_dofs, + std::vector constrained_dofs, Vector prescribed_values, + SparsePattern sparse_pattern) + : full_dof_count_{full_dof_count}, + free_equations_{std::move(free_equations)}, + element_scatters_{std::move(element_scatters)}, + shell_element_scatters_{std::move(shell_element_scatters)}, + free_dofs_{std::move(free_dofs)}, + constrained_dofs_{std::move(constrained_dofs)}, + prescribed_values_{std::move(prescribed_values)}, + sparse_pattern_{std::move(sparse_pattern)} {} -} // namespace fesa +} // namespace fesa diff --git a/src/fesa/io/hdf5/hdf5_results_writer.cpp b/src/fesa/io/hdf5/hdf5_results_writer.cpp index ceebfec..33ffaa4 100644 --- a/src/fesa/io/hdf5/hdf5_results_writer.cpp +++ b/src/fesa/io/hdf5/hdf5_results_writer.cpp @@ -3,9 +3,9 @@ #include "fesa/io/hdf5/hdf5_results_writer.hpp" -#include "fesa/analysis/analysis_model.hpp" +#include "fesa/analysis/analysis_model.h" #include "fesa/build_info.h" -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include @@ -389,38 +389,38 @@ Status validateShellWriterInput( std::size_t expectedRows = 0U; if (!sizeProductFits( domain.ShellElements().size(), kShellLocationCount, expectedRows) || - state.shellResults().size() != expectedRows) { + state.ShellResults().size() != expectedRows) { return outputFailure( "invalid-result-rows", "Shell output requires exactly GP1 through GP4 for every shell element."); } const double gauss = 1.0 / std::sqrt(3.0); const std::array locations{ - ShellMidsurfaceLocation::gp1, - ShellMidsurfaceLocation::gp2, - ShellMidsurfaceLocation::gp3, - ShellMidsurfaceLocation::gp4}; + ShellMidsurfaceLocation::kGp1, + ShellMidsurfaceLocation::kGp2, + ShellMidsurfaceLocation::kGp3, + ShellMidsurfaceLocation::kGp4}; const std::array, kShellLocationCount> coordinates{{ {-gauss, -gauss}, {gauss, -gauss}, {gauss, gauss}, {-gauss, gauss}}}; const std::array positions{ - ShellSectionPosition::bottom, - ShellSectionPosition::middle, - ShellSectionPosition::top}; + ShellSectionPosition::kBottom, + ShellSectionPosition::kMiddle, + ShellSectionPosition::kTop}; constexpr std::array zeta{-1.0, 0.0, 1.0}; for (std::size_t rowIndex = 0U; - rowIndex < state.shellResults().size(); + rowIndex < state.ShellResults().size(); ++rowIndex) { - const auto& row = state.shellResults()[rowIndex]; + const auto& row = state.ShellResults()[rowIndex]; const std::size_t element = rowIndex / kShellLocationCount; const std::size_t location = rowIndex % kShellLocationCount; if (row.element != element || row.location != locations[location] || - row.naturalCoordinates != coordinates[location] || - !isOrthonormalRightHanded(row.localFrame) || - !isFinite(row.generalizedStrain) || - !isFinite(row.sectionResultant)) { + row.natural_coordinates != coordinates[location] || + !isOrthonormalRightHanded(row.local_frame) || + !isFinite(row.generalized_strain) || + !isFinite(row.section_resultant)) { return outputFailure( "invalid-result-rows", "Shell result rows must be finite and preserve element/GP/frame identity."); @@ -437,9 +437,9 @@ Status validateShellWriterInput( } } } - if (!std::isfinite(state.physicalStrainEnergy()) || - !isFinite(state.equilibrium()) || - !isFinite(state.verificationMetrics())) { + if (!std::isfinite(state.PhysicalStrainEnergy()) || + !isFinite(state.Equilibrium()) || + !isFinite(state.VerificationMetrics())) { return outputFailure( "invalid-result-rows", "Shell energy, equilibrium, and verification metrics must be finite."); @@ -457,8 +457,8 @@ Status validateWriterInput( return outputFailure( "invalid-output-path", "The HDF5 output path must name a file."); } - if (state.identity().stepName != kStepName || - state.identity().frameIndex != kFrameIndex) { + if (state.Identity().step_name != kStepName || + state.Identity().frame_index != kFrameIndex) { return outputFailure( "invalid-result-state", "Schema v0 requires literal Step-1 and frame index 0."); @@ -470,7 +470,7 @@ Status validateWriterInput( if (!shellValidation.IsOk()) { return shellValidation; } - } else if (!state.shellResults().empty()) { + } else if (!state.ShellResults().empty()) { return outputFailure( "invalid-result-rows", "Beam output cannot contain shell recovery rows."); @@ -482,11 +482,11 @@ Status validateWriterInput( "invalid-result-state", "The nodal result shape overflows size_t."); } const std::array vectors = { - &state.displacement(), - &state.externalForce(), - &state.internalForce(), - &state.residual(), - &state.reaction()}; + &state.Displacement(), + &state.ExternalForce(), + &state.InternalForce(), + &state.Residual(), + &state.Reaction()}; for (const Vector* vector : vectors) { if (vector->Size() != fullDofCount) { return outputFailure( @@ -542,42 +542,42 @@ Status validateWriterInput( std::size_t gaussCount = 0U; if (!sizeProductFits(domain.Elements().size(), kEndpointCount, endpointCount) || !sizeProductFits(domain.Elements().size(), kGaussPointCount, gaussCount) || - state.endpointResults().size() != endpointCount || - state.gaussResults().size() != gaussCount) { + state.EndpointResults().size() != endpointCount || + state.GaussResults().size() != gaussCount) { return outputFailure( "invalid-result-rows", "Endpoint and Gauss row counts must match every element and location."); } for (std::size_t rowIndex = 0U; - rowIndex < state.endpointResults().size(); + rowIndex < state.EndpointResults().size(); ++rowIndex) { const EntityIndex expectedElement = static_cast(rowIndex / kEndpointCount); const int expectedEndpoint = static_cast(rowIndex % kEndpointCount); - const EndpointResultRow& row = state.endpointResults()[rowIndex]; + const EndpointResultRow& row = state.EndpointResults()[rowIndex]; const auto& element = domain.Elements()[expectedElement]; const auto& expectedNode = domain.Nodes()[element.node_indices[static_cast(expectedEndpoint)]]; if (row.element != expectedElement || row.endpoint != expectedEndpoint || !sameIdentity(row.node, expectedNode.source_id) || - !isFinite(row.endAction) || !isFinite(row.sectionResultant)) { + !isFinite(row.end_action) || !isFinite(row.section_resultant)) { return outputFailure( "invalid-result-rows", "Endpoint result rows must follow element/endpoint order and identity."); } } for (std::size_t rowIndex = 0U; - rowIndex < state.gaussResults().size(); + rowIndex < state.GaussResults().size(); ++rowIndex) { const EntityIndex expectedElement = static_cast(rowIndex / kGaussPointCount); const int expectedGaussPoint = static_cast(rowIndex % kGaussPointCount) + 1; - const GaussResultRow& row = state.gaussResults()[rowIndex]; + const GaussResultRow& row = state.GaussResults()[rowIndex]; if (row.element != expectedElement || - row.gaussPoint != expectedGaussPoint || - !isFinite(row.generalizedStrain) || - !isFinite(row.generalizedResultant)) { + row.gauss_point != expectedGaussPoint || + !isFinite(row.generalized_strain) || + !isFinite(row.generalized_resultant)) { return outputFailure( "invalid-result-rows", "Gauss result rows must follow element/Gauss order and identity."); @@ -594,19 +594,19 @@ Status validateWriterInput( for (std::size_t gauss = 0U; gauss < kGaussPointCount; ++gauss) { const std::size_t count = sectionPoints.empty() ? 1U : sectionPoints.size(); for (std::size_t point = 0U; point < count; ++point) { - if (stressIndex >= state.stressResults().size()) { + if (stressIndex >= state.StressResults().size()) { return outputFailure( "invalid-result-rows", "Axial stress rows are missing required element/Gauss/section locations."); } - const StressS11Row& row = state.stressResults()[stressIndex++]; + const StressS11Row& row = state.StressResults()[stressIndex++]; const std::size_t expectedPoint = sectionPoints.empty() ? 0U : point + 1U; const double expectedX1 = sectionPoints.empty() ? 0.0 : sectionPoints[point][0U]; const double expectedX2 = sectionPoints.empty() ? 0.0 : sectionPoints[point][1U]; const char* expectedSource = sectionPoints.empty() ? "fesa-default" : "input"; if (row.element != static_cast(elementIndex) || - row.gaussPoint != static_cast(gauss + 1U) || - row.sectionPoint != expectedPoint || + row.gauss_point != static_cast(gauss + 1U) || + row.section_point != expectedPoint || row.x1 != expectedX1 || row.x2 != expectedX2 || row.source != expectedSource || !isValidUtf8(row.source) || !std::isfinite(row.x1) || !std::isfinite(row.x2) || @@ -618,7 +618,7 @@ Status validateWriterInput( } } } - if (stressIndex != state.stressResults().size()) { + if (stressIndex != state.StressResults().size()) { return outputFailure( "invalid-result-rows", "Axial stress output contains extra rows."); } @@ -635,7 +635,7 @@ Status validateWriterInput( } - auto analysisModelResult = AnalysisModel::create(domain); + auto analysisModelResult = AnalysisModel::Create(domain); if (!analysisModelResult.HasValue()) { return outputFailure( "invalid-result-state", @@ -643,7 +643,7 @@ Status validateWriterInput( } const AnalysisModel analysisModel = std::move(analysisModelResult.Value()); - auto dofResult = DofManager::create(analysisModel); + auto dofResult = DofManager::Create(analysisModel); if (!dofResult.HasValue()) { return outputFailure( "invalid-result-state", @@ -652,16 +652,16 @@ Status validateWriterInput( const DofManager dofs = std::move(dofResult.Value()); modelData.constraintMask.assign(fullDofCount, 0U); modelData.prescribedDisplacement.assign(fullDofCount, 0.0); - if (dofs.constrainedDofs().size() != dofs.prescribedValues().Size()) { + if (dofs.ConstrainedDofs().size() != dofs.PrescribedValues().Size()) { return outputFailure( "invalid-result-state", "Constraint identities and prescribed values have inconsistent sizes."); } for (std::size_t index = 0U; - index < dofs.constrainedDofs().size(); + index < dofs.ConstrainedDofs().size(); ++index) { - const std::size_t fullDof = dofs.constrainedDofs()[index]; - const double prescribed = dofs.prescribedValues()[index]; + const std::size_t fullDof = dofs.ConstrainedDofs()[index]; + const double prescribed = dofs.PrescribedValues()[index]; if (fullDof >= fullDofCount || !std::isfinite(prescribed)) { return outputFailure( "invalid-result-state", @@ -1422,9 +1422,9 @@ std::vector flattenEndpointValues( for (const auto& row : rows) { if (sectionResultants) { values.insert( - values.end(), row.sectionResultant.begin(), row.sectionResultant.end()); + values.end(), row.section_resultant.begin(), row.section_resultant.end()); } else { - values.insert(values.end(), row.endAction.begin(), row.endAction.end()); + values.insert(values.end(), row.end_action.begin(), row.end_action.end()); } } return values; @@ -1437,7 +1437,7 @@ std::vector flattenGaussValues( values.reserve(rows.size() * kGeneralizedComponentCount); for (const auto& row : rows) { const auto& rowValues = - resultants ? row.generalizedResultant : row.generalizedStrain; + resultants ? row.generalized_resultant : row.generalized_strain; values.insert(values.end(), rowValues.begin(), rowValues.end()); } return values; @@ -1445,12 +1445,12 @@ std::vector flattenGaussValues( void writeStress(const hid_t file, const AnalysisState& state) { std::vector rows; - rows.reserve(state.stressResults().size()); - for (const auto& row : state.stressResults()) { + rows.reserve(state.StressResults().size()); + for (const auto& row : state.StressResults()) { rows.push_back({ static_cast(row.element), - static_cast(row.gaussPoint), - static_cast(row.sectionPoint), + static_cast(row.gauss_point), + static_cast(row.section_point), row.x1, row.x2, row.source.c_str(), @@ -1544,26 +1544,26 @@ void writeShellResultDatasets( std::vector generalizedStrains; std::vector sectionResultants; std::vector stresses; - localFrames.reserve(state.shellResults().size() * 9U); + localFrames.reserve(state.ShellResults().size() * 9U); generalizedStrains.reserve( - state.shellResults().size() * kShellGeneralizedComponentCount); + state.ShellResults().size() * kShellGeneralizedComponentCount); sectionResultants.reserve( - state.shellResults().size() * kShellGeneralizedComponentCount); + state.ShellResults().size() * kShellGeneralizedComponentCount); stresses.reserve( - state.shellResults().size() * kShellSectionPositionCount * + state.ShellResults().size() * kShellSectionPositionCount * kShellStressComponentCount); - for (const auto& row : state.shellResults()) { - for (const auto& axis : row.localFrame) { + for (const auto& row : state.ShellResults()) { + for (const auto& axis : row.local_frame) { localFrames.insert(localFrames.end(), axis.begin(), axis.end()); } generalizedStrains.insert( generalizedStrains.end(), - row.generalizedStrain.begin(), - row.generalizedStrain.end()); + row.generalized_strain.begin(), + row.generalized_strain.end()); sectionResultants.insert( sectionResultants.end(), - row.sectionResultant.begin(), - row.sectionResultant.end()); + row.section_resultant.begin(), + row.section_resultant.end()); for (const auto& position : row.stress) { stresses.insert( stresses.end(), @@ -1624,22 +1624,22 @@ void writeShellResultDatasets( "shell-local", "section-position"); writeShellResultIdentity(file, stressPath, true); - const double energy = state.physicalStrainEnergy(); + const double energy = state.PhysicalStrainEnergy(); writeDoubleDataset( file, std::string{kStepRoot} + "/global/energy", {1U}, &energy, 1U, "PHYSICAL_STRAIN_ENERGY", "force*length", "global", "global"); writeDoubleDataset( file, std::string{kStepRoot} + "/global/equilibrium", {6U}, - state.equilibrium().data(), state.equilibrium().size(), + state.Equilibrium().data(), state.Equilibrium().size(), "FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "global-origin"); const std::string metricsPath = std::string{kStepRoot} + "/global/verification_metrics"; writeDoubleDataset( - file, metricsPath, {3U}, state.verificationMetrics().data(), - state.verificationMetrics().size(), + file, metricsPath, {3U}, state.VerificationMetrics().data(), + state.VerificationMetrics().size(), "FREE_RESIDUAL_NORMALIZED,FORCE_BALANCE_NORMALIZED,MOMENT_BALANCE_NORMALIZED", "1,1,1", "global", "verification"); { @@ -1735,8 +1735,8 @@ void writeResultDatasets( file, std::string{kStepRoot} + "/nodal/displacement", nodalDimensions, - state.displacement().Data(), - state.displacement().Size(), + state.Displacement().Data(), + state.Displacement().Size(), "UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian", "global-cartesian", @@ -1745,8 +1745,8 @@ void writeResultDatasets( file, std::string{kStepRoot} + "/nodal/reaction", nodalDimensions, - state.reaction().Data(), - state.reaction().Size(), + state.Reaction().Data(), + state.Reaction().Size(), "RF1,RF2,RF3,RM1,RM2,RM3", "force,force,force,force*length,force*length,force*length", "global-cartesian", @@ -1765,7 +1765,7 @@ void writeResultDatasets( static_cast(domain.Elements().size()), kEndpointCount, kGeneralizedComponentCount}; - const auto endActions = flattenEndpointValues(state.endpointResults(), false); + const auto endActions = flattenEndpointValues(state.EndpointResults(), false); writeDoubleDataset( file, std::string{kStepRoot} + "/element/end_force_local", @@ -1777,7 +1777,7 @@ void writeResultDatasets( "beam-local", "endpoint-outward-action"); const auto sectionResultants = - flattenEndpointValues(state.endpointResults(), true); + flattenEndpointValues(state.EndpointResults(), true); writeDoubleDataset( file, std::string{kStepRoot} + "/element/section_resultant", @@ -1789,7 +1789,7 @@ void writeResultDatasets( "beam-local", "endpoint-positive-local-x-section-cut"); const auto generalizedStrains = - flattenGaussValues(state.gaussResults(), false); + flattenGaussValues(state.GaussResults(), false); writeDoubleDataset( file, std::string{kStepRoot} + "/element/generalized_strain", @@ -1801,7 +1801,7 @@ void writeResultDatasets( "beam-local", "integration-point"); const auto generalizedResultants = - flattenGaussValues(state.gaussResults(), true); + flattenGaussValues(state.GaussResults(), true); writeDoubleDataset( file, std::string{kStepRoot} + "/element/generalized_resultant", @@ -2494,7 +2494,7 @@ void selfCheckFile( "beam-local", "integration-point"); requireCompoundDataset( file.get(), "/steps/Step-1/frames/0/element/stress_s11", - static_cast(state.stressResults().size()), + static_cast(state.StressResults().size()), {"internal_element_id", "gauss_point_index", "section_point_index", "x1", "x2", "source", "S11"}); auto stress = openDatasetForCheck( @@ -2563,7 +2563,7 @@ bool finalizeFile( } // namespace -Status Hdf5ResultsWriter::write( +Status Hdf5ResultsWriter::Write( const std::filesystem::path& outputPath, const Domain& domain, const AnalysisState& state, diff --git a/src/fesa/math/sparse_matrix.cpp b/src/fesa/math/sparse_matrix.cpp index 971c379..f0ff8c8 100644 --- a/src/fesa/math/sparse_matrix.cpp +++ b/src/fesa/math/sparse_matrix.cpp @@ -8,7 +8,7 @@ #include #include -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" namespace fesa { namespace { @@ -79,8 +79,8 @@ Result SparseMatrix::FromCoo( std::vector contributions, const SparsePattern& expected_pattern) { const Status pattern_status = - ValidateCsr(rows, columns, expected_pattern.rowOffsets, - expected_pattern.columnIndices, nullptr); + ValidateCsr(rows, columns, expected_pattern.row_offsets, + expected_pattern.column_indices, nullptr); if (!pattern_status.IsOk()) { return Result::Failure(pattern_status); } @@ -113,12 +113,12 @@ Result SparseMatrix::FromCoo( right.local_order); }); - std::vector values(expected_pattern.columnIndices.size(), 0.0); + std::vector values(expected_pattern.column_indices.size(), 0.0); for (const auto& contribution : contributions) { - const std::size_t begin = expected_pattern.rowOffsets[contribution.row]; - const std::size_t end = expected_pattern.rowOffsets[contribution.row + 1U]; - const auto first = expected_pattern.columnIndices.begin() + begin; - const auto last = expected_pattern.columnIndices.begin() + end; + const std::size_t begin = expected_pattern.row_offsets[contribution.row]; + const std::size_t end = expected_pattern.row_offsets[contribution.row + 1U]; + const auto first = expected_pattern.column_indices.begin() + begin; + const auto last = expected_pattern.column_indices.begin() + end; const auto found = std::lower_bound(first, last, contribution.column); if (found == last || *found != contribution.column) { return Result::Failure(SparseFailure( @@ -129,7 +129,7 @@ Result SparseMatrix::FromCoo( } const std::size_t position = static_cast( - std::distance(expected_pattern.columnIndices.begin(), found)); + std::distance(expected_pattern.column_indices.begin(), found)); values[position] += contribution.value; if (!std::isfinite(values[position])) { return Result::Failure(SparseFailure( @@ -140,8 +140,8 @@ Result SparseMatrix::FromCoo( } } - SparseMatrix matrix{rows, columns, expected_pattern.rowOffsets, - expected_pattern.columnIndices, std::move(values)}; + SparseMatrix matrix{rows, columns, expected_pattern.row_offsets, + expected_pattern.column_indices, std::move(values)}; const Status status = matrix.Validate(); if (!status.IsOk()) { return Result::Failure(status); diff --git a/src/fesa/results/result_recovery.cpp b/src/fesa/results/result_recovery.cpp index 67d92c5..69cb79f 100644 --- a/src/fesa/results/result_recovery.cpp +++ b/src/fesa/results/result_recovery.cpp @@ -1,7 +1,4 @@ -#include "fesa/results/result_recovery.hpp" - -#include "fesa/elements/euler_beam_3d.h" -#include "fesa/elements/mitc4_shell.h" +#include "fesa/results/result_recovery.h" #include #include @@ -17,6 +14,9 @@ #include #include +#include "fesa/elements/euler_beam_3d.h" +#include "fesa/elements/mitc4_shell.h" + namespace fesa { namespace { @@ -30,1096 +30,974 @@ constexpr double kAxisTolerance = 1.0e-12; using AxisSet = std::array, 3>; -Status recoveryFailure(const std::string& code, - const SourceLocation& location, +Status RecoveryFailure(const std::string& code, const SourceLocation& location, const std::string& identity, const std::string& message) { - return Status::Failure( - FailureCategory::kModel, - {{Severity::kError, - code, - location, - "RESULT_RECOVERY", - identity, - message}}); + return Status::Failure(FailureCategory::kModel, + {{Severity::kError, code, location, "RESULT_RECOVERY", + identity, message}}); } -template -Result recoveryResultFailure(const std::string& code, +template +Result RecoveryResultFailure(const std::string& code, const SourceLocation& location, const std::string& identity, const std::string& message) { - return Result::Failure( - recoveryFailure(code, location, identity, message)); + return Result::Failure(RecoveryFailure(code, location, identity, message)); } -bool sameSourceIdentity(const SourceEntityId& left, +bool SameSourceIdentity(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; + return left.instance_name == right.instance_name && + left.source_label == right.source_label && + left.source_label_text == right.source_label_text; } -template -bool finite(const std::array& values) { - return std::all_of( - values.begin(), values.end(), - [](const double value) { return std::isfinite(value); }); +template +bool IsFinite(const std::array& values) { + return std::all_of(values.begin(), values.end(), + [](const double value) { return std::isfinite(value); }); } -bool finite(const Vector& values) { - for (std::size_t index = 0U; index < values.Size(); ++index) { - if (!std::isfinite(values[index])) { - return false; - } +bool IsFinite(const Vector& values) { + for (std::size_t index = 0U; index < values.Size(); ++index) { + if (!std::isfinite(values[index])) { + return false; } - return true; + } + return true; } -double indexedNorm(const Vector& values, +double IndexedNorm(const Vector& values, const std::vector& indices) { - double result = 0.0; - for (const std::size_t index : indices) { - result = std::hypot(result, values[index]); - } - return result; + double result = 0.0; + for (const std::size_t index : indices) { + result = std::hypot(result, values[index]); + } + return result; } -std::array freeEquationInternalTermNorms( - const SparseMatrix& stiffness, - const Vector& displacement, +/// @brief Computes separate Kff*df and Kfc*dc norms in stable CSR order. +std::array FreeEquationInternalTermNorms( + const SparseMatrix& stiffness, const Vector& displacement, const DofManager& dofs) { - std::vector freeColumn(dofs.fullDofCount(), 0U); - for (const std::size_t fullDof : dofs.freeDofs()) { - freeColumn[fullDof] = 1U; - } + std::vector free_column(dofs.FullDofCount(), 0U); + for (const std::size_t full_dof : dofs.FreeDofs()) { + free_column[full_dof] = 1U; + } - double freeTermNorm = 0.0; - double constrainedTermNorm = 0.0; - for (const std::size_t row : dofs.freeDofs()) { - double freeTerm = 0.0; - double constrainedTerm = 0.0; - for (std::size_t position = stiffness.RowOffsets()[row]; - position < stiffness.RowOffsets()[row + 1U]; - ++position) { - const std::size_t column = stiffness.ColumnIndices()[position]; - const double contribution = - stiffness.Values()[position] * displacement[column]; - if (freeColumn[column] != 0U) { - freeTerm += contribution; - } else { - constrainedTerm += contribution; - } - } - freeTermNorm = std::hypot(freeTermNorm, freeTerm); - constrainedTermNorm = - std::hypot(constrainedTermNorm, constrainedTerm); + double free_term_norm = 0.0; + double constrained_term_norm = 0.0; + for (const std::size_t row : dofs.FreeDofs()) { + double free_term = 0.0; + double constrained_term = 0.0; + for (std::size_t position = stiffness.RowOffsets()[row]; + position < stiffness.RowOffsets()[row + 1U]; ++position) { + const std::size_t column = stiffness.ColumnIndices()[position]; + const double contribution = + stiffness.Values()[position] * displacement[column]; + if (free_column[column] != 0U) { + free_term += contribution; + } else { + constrained_term += contribution; + } } - return {freeTermNorm, constrainedTermNorm}; + free_term_norm = std::hypot(free_term_norm, free_term); + constrained_term_norm = std::hypot(constrained_term_norm, constrained_term); + } + return {free_term_norm, constrained_term_norm}; } -bool strictlyIncreasing(const std::vector& values) { - return std::adjacent_find( - values.begin(), values.end(), - [](const std::size_t left, const std::size_t right) { - return left >= right; - }) == values.end(); +/// @brief Checks that equation-space indices preserve stable full-DOF order. +bool StrictlyIncreasing(const std::vector& values) { + return std::adjacent_find( + values.begin(), values.end(), + [](const std::size_t left, const std::size_t right) { + return left >= right; + }) == values.end(); } -Status validateRecoveryInputs(const AnalysisModel& model, +/// @brief Validates complete recovery inputs before creating candidate results. +Status ValidateRecoveryInputs(const AnalysisModel& model, const DofManager& dofs, - const SparseMatrix& fullStiffness, + const SparseMatrix& full_stiffness, const AnalysisState& state) { - const Domain& domain = model.domain(); - if (domain.Nodes().size() > - (std::numeric_limits::max)() / kDofsPerNode) { - return recoveryFailure( - "invalid-recovery-dimensions", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "The semantic node count cannot be represented in full-DOF space."); - } - const std::size_t fullCount = domain.Nodes().size() * kDofsPerNode; - if (dofs.fullDofCount() != fullCount || - fullStiffness.Rows() != fullCount || - fullStiffness.Columns() != fullCount || - state.displacement().Size() != fullCount || - state.externalForce().Size() != fullCount || - state.internalForce().Size() != fullCount || - state.residual().Size() != fullCount || - state.reaction().Size() != fullCount) { - return recoveryFailure( - "invalid-recovery-dimensions", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "Model, DOF, stiffness, and AnalysisState full-space dimensions must agree."); - } - const Status matrixStatus = fullStiffness.Validate(); - if (!matrixStatus.IsOk()) { - return matrixStatus; - } - if (!finite(state.displacement()) || !finite(state.externalForce())) { - return recoveryFailure( - "nonfinite-recovery-value", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "Displacement and external-force inputs must be finite."); - } + const Domain& domain = model.GetDomain(); + if (domain.Nodes().size() > + (std::numeric_limits::max)() / kDofsPerNode) { + return RecoveryFailure( + "invalid-recovery-dimensions", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "The semantic node count cannot be represented in full-DOF space."); + } + const std::size_t full_count = domain.Nodes().size() * kDofsPerNode; + if (dofs.FullDofCount() != full_count || + full_stiffness.Rows() != full_count || + full_stiffness.Columns() != full_count || + state.Displacement().Size() != full_count || + state.ExternalForce().Size() != full_count || + state.InternalForce().Size() != full_count || + state.Residual().Size() != full_count || + state.Reaction().Size() != full_count) { + return RecoveryFailure("invalid-recovery-dimensions", + {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "Model, DOF, stiffness, and AnalysisState " + "full-space dimensions must agree."); + } + const Status matrix_status = full_stiffness.Validate(); + if (!matrix_status.IsOk()) { + return matrix_status; + } + if (!IsFinite(state.Displacement()) || !IsFinite(state.ExternalForce())) { + return RecoveryFailure( + "nonfinite-recovery-value", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "Displacement and external-force inputs must be finite."); + } - const auto& freeDofs = dofs.freeDofs(); - const auto& constrainedDofs = dofs.constrainedDofs(); - if (freeDofs.size() != dofs.freeDofCount() || - constrainedDofs.size() != dofs.constrainedDofCount() || - dofs.prescribedValues().Size() != constrainedDofs.size() || - freeDofs.size() + constrainedDofs.size() != fullCount || - !strictlyIncreasing(freeDofs) || - !strictlyIncreasing(constrainedDofs)) { - return recoveryFailure( - "invalid-recovery-order", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "Free and constrained DOFs must form stable increasing full-space orders."); + const auto& free_dofs = dofs.FreeDofs(); + const auto& constrained_dofs = dofs.ConstrainedDofs(); + if (free_dofs.size() != dofs.FreeDofCount() || + constrained_dofs.size() != dofs.ConstrainedDofCount() || + dofs.PrescribedValues().Size() != constrained_dofs.size() || + free_dofs.size() + constrained_dofs.size() != full_count || + !StrictlyIncreasing(free_dofs) || !StrictlyIncreasing(constrained_dofs)) { + return RecoveryFailure("invalid-recovery-order", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "Free and constrained DOFs must form stable " + "increasing full-space orders."); + } + std::vector ownership(full_count, 0U); + try { + for (std::size_t equation = 0U; equation < free_dofs.size(); ++equation) { + const std::size_t full_dof = free_dofs[equation]; + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof) != equation) { + return RecoveryFailure( + "invalid-recovery-order", {domain.SourcePath(), 0U}, + std::to_string(full_dof), + "Free equations must match stable full-DOF order."); + } + ownership[full_dof] = 1U; + } + for (std::size_t constrained = 0U; constrained < constrained_dofs.size(); + ++constrained) { + const std::size_t full_dof = constrained_dofs[constrained]; + if (full_dof >= full_count || ownership[full_dof] != 0U || + dofs.FreeEquation(full_dof).has_value()) { + return RecoveryFailure( + "invalid-recovery-order", {domain.SourcePath(), 0U}, + std::to_string(full_dof), + "Constrained DOFs must be unique and absent from free equations."); + } + if (!std::isfinite(dofs.PrescribedValues()[constrained]) || + state.Displacement()[full_dof] != + dofs.PrescribedValues()[constrained]) { + return RecoveryFailure("invalid-recovery-state", + {domain.SourcePath(), 0U}, + std::to_string(full_dof), + "Constrained displacement must equal its " + "prescribed value before recovery."); + } + ownership[full_dof] = 2U; + } + } catch (const std::out_of_range&) { + return RecoveryFailure( + "invalid-recovery-dimensions", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "DofManager equation storage must cover every full DOF."); + } + if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { + return RecoveryFailure( + "invalid-recovery-order", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "Free and constrained DOFs must partition the full range."); + } + + EntityIndex previous_element = 0U; + bool first_element = true; + for (const EntityIndex element : model.ActiveElements()) { + if (element >= domain.Elements().size() || + (!first_element && element <= previous_element)) { + return RecoveryFailure( + "invalid-recovery-entity", {domain.SourcePath(), 0U}, + std::to_string(element), + "Active elements must be unique in stable internal-index order."); + } + first_element = false; + previous_element = element; + const auto& definition = domain.Elements()[element]; + if (definition.node_indices[0U] >= domain.Nodes().size() || + definition.node_indices[1U] >= domain.Nodes().size() || + definition.material_index >= domain.Materials().size() || + definition.section_index >= domain.Sections().size()) { + return RecoveryFailure( + "invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Active beam references must resolve before recovery."); } - std::vector ownership(fullCount, 0U); try { - for (std::size_t equation = 0U; equation < freeDofs.size(); ++equation) { - const std::size_t fullDof = freeDofs[equation]; - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof) != equation) { - return recoveryFailure( - "invalid-recovery-order", - {domain.SourcePath(), 0U}, - std::to_string(fullDof), - "Free equations must match stable full-DOF order."); - } - ownership[fullDof] = 1U; - } - for (std::size_t constrained = 0U; - constrained < constrainedDofs.size(); - ++constrained) { - const std::size_t fullDof = constrainedDofs[constrained]; - if (fullDof >= fullCount || ownership[fullDof] != 0U || - dofs.freeEquation(fullDof).has_value()) { - return recoveryFailure( - "invalid-recovery-order", - {domain.SourcePath(), 0U}, - std::to_string(fullDof), - "Constrained DOFs must be unique and absent from free equations."); - } - if (!std::isfinite(dofs.prescribedValues()[constrained]) || - state.displacement()[fullDof] != - dofs.prescribedValues()[constrained]) { - return recoveryFailure( - "invalid-recovery-state", - {domain.SourcePath(), 0U}, - std::to_string(fullDof), - "Constrained displacement must equal its prescribed value before recovery."); - } - ownership[fullDof] = 2U; - } - } catch (const std::out_of_range&) { - return recoveryFailure( - "invalid-recovery-dimensions", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "DofManager equation storage must cover every full DOF."); - } - if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { - return recoveryFailure( - "invalid-recovery-order", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "Free and constrained DOFs must partition the full range."); - } - - EntityIndex previousElement = 0U; - bool firstElement = true; - for (const EntityIndex element : model.activeElements()) { - if (element >= domain.Elements().size() || - (!firstElement && element <= previousElement)) { - return recoveryFailure( - "invalid-recovery-entity", - {domain.SourcePath(), 0U}, - std::to_string(element), - "Active elements must be unique in stable internal-index order."); - } - firstElement = false; - previousElement = element; - const auto& definition = domain.Elements()[element]; - if (definition.node_indices[0U] >= domain.Nodes().size() || - definition.node_indices[1U] >= domain.Nodes().size() || - definition.material_index >= domain.Materials().size() || - definition.section_index >= domain.Sections().size()) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Active beam references must resolve before recovery."); - } - try { - const auto& scatter = dofs.elementScatter(element); - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - for (std::size_t component = 0U; - component < kDofsPerNode; - ++component) { - const std::size_t expected = - static_cast(definition.node_indices[endpoint]) * - kDofsPerNode + - component; - if (scatter[endpoint * kDofsPerNode + component] != expected || - expected >= fullCount) { - return recoveryFailure( - "invalid-recovery-order", - definition.location, - definition.source_id.source_label_text, - "Element scatter must preserve endpoint/component full-DOF order."); - } - } - } - } catch (const std::out_of_range&) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Every active element requires one twelve-DOF scatter map."); - } - } - - if (!model.activeElements().empty() && !domain.ShellElements().empty()) { - return recoveryFailure( - "unsupported-mixed-element-model", - {domain.SourcePath(), 0U}, - "B33:FESA-MITC4", - "Result recovery does not support mixed beam and shell models."); - } - if (domain.ShellElements().size() > - static_cast( - (std::numeric_limits::max)())) { - return recoveryFailure( - "invalid-recovery-dimensions", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "The shell element count cannot be represented by stable element identities."); - } - for (std::size_t elementOrder = 0U; - elementOrder < domain.ShellElements().size(); - ++elementOrder) { - const auto& definition = domain.ShellElements()[elementOrder]; - if (definition.material_index >= domain.Materials().size() || - definition.section_index >= domain.ShellSections().size()) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Active shell material and section references must resolve before recovery."); - } - try { - const auto& scatter = dofs.shellElementScatter( - static_cast(elementOrder)); - for (std::size_t nodePosition = 0U; - nodePosition < definition.node_indices.size(); - ++nodePosition) { - const EntityIndex node = definition.node_indices[nodePosition]; - if (node >= domain.Nodes().size()) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Active shell node references must resolve before recovery."); - } - for (std::size_t component = 0U; - component < kDofsPerNode; - ++component) { - const std::size_t local = - nodePosition * kDofsPerNode + component; - const std::size_t expected = - static_cast(node) * kDofsPerNode + - component; - if (scatter[local] != expected || expected >= fullCount) { - return recoveryFailure( - "invalid-recovery-order", - definition.location, - definition.source_id.source_label_text, - "Shell scatter must preserve node/component full-DOF order."); - } - } - } - } catch (const std::out_of_range&) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Every active shell requires one twenty-four-DOF scatter map."); - } - } - return Status::Ok(); -} - -char asciiLower(const char value) { - if (value >= 'A' && value <= 'Z') { - return static_cast(value + ('a' - 'A')); - } - return value; -} - -bool equalName(const std::string& left, const std::string& right) { - return left.size() == right.size() && - std::equal( - left.begin(), left.end(), right.begin(), - [](const char leftValue, const char rightValue) { - return asciiLower(leftValue) == asciiLower(rightValue); - }); -} - -bool tryPositiveInteger(const std::string& text, std::int64_t& value) { - const char* const first = text.data(); - const char* const last = first + text.size(); - const auto parsed = std::from_chars(first, last, value); - return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; -} - -Result> resolveLoadTarget(const Domain& domain, - const NodalLoad& load) { - std::vector sets; - for (const auto& set : domain.NodeSets()) { - if (equalName(set.name, load.target)) { - sets.push_back(&set); - } - } - std::vector nodes; - std::int64_t sourceLabel = 0; - if (tryPositiveInteger(load.target, sourceLabel)) { - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - if (domain.Nodes()[node].source_id.source_label == sourceLabel) { - nodes.push_back(static_cast(node)); - } - } - } - if (sets.size() > 1U || nodes.size() > 1U || - (!sets.empty() && !nodes.empty())) { - return recoveryResultFailure>( - "invalid-node-station-entity", - load.location, - load.target, - "A station-eligibility load target must resolve unambiguously."); - } - if (!sets.empty()) { - std::vector seen(domain.Nodes().size(), 0U); - for (const EntityIndex node : sets.front()->node_indices) { - if (node >= domain.Nodes().size() || seen[node] != 0U) { - return recoveryResultFailure>( - "invalid-node-station-entity", - load.location, - load.target, - "A station-eligibility node set must contain unique valid nodes."); - } - seen[node] = 1U; - } - return Result>::Success( - sets.front()->node_indices); - } - if (!nodes.empty()) { - return Result>::Success(std::move(nodes)); - } - return recoveryResultFailure>( - "invalid-node-station-entity", - load.location, - load.target, - "A station-eligibility load target must resolve to a node or node set."); -} - -std::array cross(const std::array& left, - const std::array& right) { - return { - left[1U] * right[2U] - left[2U] * right[1U], - left[2U] * right[0U] - left[0U] * right[2U], - left[0U] * right[1U] - left[1U] * right[0U]}; -} - -double dot(const std::array& left, - const std::array& right) { - return left[0U] * right[0U] + left[1U] * right[1U] + - left[2U] * right[2U]; -} - -double norm(const std::array& value) { - return std::hypot(value[0U], value[1U], value[2U]); -} - -bool accumulateVectorAndScale( - std::array& total, - double& scale, - const std::array& contribution) { - const double magnitude = norm(contribution); - const double accumulatedScale = scale + magnitude; - if (!finite(contribution) || !std::isfinite(magnitude) || - !std::isfinite(accumulatedScale)) { - return false; - } - for (std::size_t component = 0U; - component < contribution.size(); - ++component) { - const double accumulated = total[component] + contribution[component]; - if (!std::isfinite(accumulated)) { - return false; - } - total[component] = accumulated; - } - scale = accumulatedScale; - return true; -} - -double normalizedBalance( - const std::array& balance, - const double scale) { - const double balanceNorm = norm(balance); - if (!std::isfinite(balanceNorm) || !std::isfinite(scale)) { - return (std::numeric_limits::infinity)(); - } - if (scale == 0.0) { - return balanceNorm == 0.0 - ? 0.0 - : (std::numeric_limits::infinity)(); - } - return balanceNorm / scale; -} - -Status populateShellGlobalEvidence( - const Domain& domain, - const DofManager& dofs, - const Vector& externalForce, - const Vector& residual, - const double normalizedResidual, - ShellStateCandidate& candidate) { - std::vector constrained(dofs.fullDofCount(), 0U); - for (const std::size_t fullDof : dofs.constrainedDofs()) { - constrained[fullDof] = 1U; - } - - std::array appliedForce{}; - std::array reactionForce{}; - std::array appliedMoment{}; - std::array reactionMoment{}; - double appliedForceScale = 0.0; - double reactionForceScale = 0.0; - double appliedMomentScale = 0.0; - double reactionMomentScale = 0.0; - for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { - const std::size_t offset = node * kDofsPerNode; - std::array nodalAppliedForce{}; - std::array nodalReactionForce{}; - std::array nodalAppliedMoment{}; - std::array nodalReactionMoment{}; - for (std::size_t component = 0U; component < 3U; ++component) { - nodalAppliedForce[component] = externalForce[offset + component]; - nodalAppliedMoment[component] = - externalForce[offset + 3U + component]; - if (constrained[offset + component] != 0U) { - nodalReactionForce[component] = residual[offset + component]; - } - if (constrained[offset + 3U + component] != 0U) { - nodalReactionMoment[component] = - residual[offset + 3U + component]; - } - } - - const auto appliedForceMoment = - cross(domain.Nodes()[node].coordinates, nodalAppliedForce); - const auto reactionForceMoment = - cross(domain.Nodes()[node].coordinates, nodalReactionForce); - for (std::size_t component = 0U; component < 3U; ++component) { - nodalAppliedMoment[component] += appliedForceMoment[component]; - nodalReactionMoment[component] += reactionForceMoment[component]; - } - if (!accumulateVectorAndScale( - appliedForce, appliedForceScale, nodalAppliedForce) || - !accumulateVectorAndScale( - reactionForce, reactionForceScale, nodalReactionForce) || - !accumulateVectorAndScale( - appliedMoment, appliedMomentScale, nodalAppliedMoment) || - !accumulateVectorAndScale( - reactionMoment, reactionMomentScale, nodalReactionMoment)) { - return recoveryFailure( - "nonfinite-recovery-value", - domain.Nodes()[node].location, - domain.Nodes()[node].source_id.source_label_text, - "Global force and moment evidence must remain finite in source-node order."); - } - } - - std::array forceBalance{}; - std::array momentBalance{}; - for (std::size_t component = 0U; component < 3U; ++component) { - forceBalance[component] = - appliedForce[component] + reactionForce[component]; - momentBalance[component] = - appliedMoment[component] + reactionMoment[component]; - candidate.equilibrium[component] = forceBalance[component]; - candidate.equilibrium[3U + component] = momentBalance[component]; - } - const double forceMetric = normalizedBalance( - forceBalance, - (std::max)(appliedForceScale, reactionForceScale)); - const double momentMetric = normalizedBalance( - momentBalance, - (std::max)(appliedMomentScale, reactionMomentScale)); - candidate.verificationMetrics = { - normalizedResidual, forceMetric, momentMetric}; - if (!finite(candidate.equilibrium) || - !finite(candidate.verificationMetrics)) { - return recoveryFailure( - "nonfinite-recovery-value", - {domain.SourcePath(), 0U}, - "global-equilibrium", - "Global equilibrium values and their physical normalization scales must be finite."); - } - if (forceMetric > kGlobalEquilibriumTolerance || - momentMetric > kGlobalEquilibriumTolerance) { - return recoveryFailure( - "global-equilibrium-tolerance-failure", - {domain.SourcePath(), 0U}, - "global-equilibrium", - "Normalized global force or moment balance exceeds 1e-10."); - } - return Status::Ok(); -} - -std::optional localAxes(const Domain& domain, - const EulerBeam3DDefinition& element) { - const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; - const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; - const std::array delta = { - second[0U] - first[0U], - second[1U] - first[1U], - second[2U] - first[2U]}; - const double length = norm(delta); - if (!std::isfinite(length) || !(length > 0.0)) { - return std::nullopt; - } - const std::array ex = { - delta[0U] / length, delta[1U] / length, delta[2U] / length}; - const auto& guide = domain.Sections()[element.section_index].first_axis; - const double projection = dot(guide, ex); - const std::array eyTrial = { - guide[0U] - projection * ex[0U], - guide[1U] - projection * ex[1U], - guide[2U] - projection * ex[2U]}; - const double eyNorm = norm(eyTrial); - if (!std::isfinite(eyNorm) || !(eyNorm > 0.0)) { - return std::nullopt; - } - const std::array ey = { - eyTrial[0U] / eyNorm, - eyTrial[1U] / eyNorm, - eyTrial[2U] / eyNorm}; - const std::array ez = cross(ex, ey); - const AxisSet axes = {ex, ey, ez}; - for (const auto& axis : axes) { - for (const double component : axis) { - if (!std::isfinite(component)) { - return std::nullopt; - } - } - } - return axes; -} - -bool sameAxes(const AxisSet& left, const AxisSet& right) { - for (std::size_t axis = 0U; axis < left.size(); ++axis) { - for (std::size_t component = 0U; - component < left[axis].size(); + const auto& scatter = dofs.ElementScatter(element); + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + for (std::size_t component = 0U; component < kDofsPerNode; ++component) { - if (std::abs(left[axis][component] - right[axis][component]) > - kAxisTolerance) { - return false; - } + const std::size_t expected = + static_cast(definition.node_indices[endpoint]) * + kDofsPerNode + + component; + if (scatter[endpoint * kDofsPerNode + component] != expected || + expected >= full_count) { + return RecoveryFailure("invalid-recovery-order", + definition.location, + definition.source_id.source_label_text, + "Element scatter must preserve " + "endpoint/component full-DOF order."); + } } + } + } catch (const std::out_of_range&) { + return RecoveryFailure( + "invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Every active element requires one twelve-DOF scatter map."); } - return true; + } + + if (!model.ActiveElements().empty() && !domain.ShellElements().empty()) { + return RecoveryFailure( + "unsupported-mixed-element-model", {domain.SourcePath(), 0U}, + "B33:FESA-MITC4", + "Result recovery does not support mixed beam and shell models."); + } + if (domain.ShellElements().size() > + static_cast((std::numeric_limits::max)())) { + return RecoveryFailure("invalid-recovery-dimensions", + {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "The shell element count cannot be represented by " + "stable element identities."); + } + for (std::size_t element_order = 0U; + element_order < domain.ShellElements().size(); ++element_order) { + const auto& definition = domain.ShellElements()[element_order]; + if (definition.material_index >= domain.Materials().size() || + definition.section_index >= domain.ShellSections().size()) { + return RecoveryFailure("invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Active shell material and section references " + "must resolve before recovery."); + } + try { + const auto& scatter = + dofs.ShellElementScatter(static_cast(element_order)); + for (std::size_t node_position = 0U; + node_position < definition.node_indices.size(); ++node_position) { + const EntityIndex node = definition.node_indices[node_position]; + if (node >= domain.Nodes().size()) { + return RecoveryFailure( + "invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Active shell node references must resolve before recovery."); + } + for (std::size_t component = 0U; component < kDofsPerNode; + ++component) { + const std::size_t local = node_position * kDofsPerNode + component; + const std::size_t expected = + static_cast(node) * kDofsPerNode + component; + if (scatter[local] != expected || expected >= full_count) { + return RecoveryFailure( + "invalid-recovery-order", definition.location, + definition.source_id.source_label_text, + "Shell scatter must preserve node/component full-DOF order."); + } + } + } + } catch (const std::out_of_range&) { + return RecoveryFailure( + "invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Every active shell requires one twenty-four-DOF scatter map."); + } + } + return Status::Ok(); } -} // namespace +char AsciiLower(const char value) { + if (value >= 'A' && value <= 'Z') { + return static_cast(value + ('a' - 'A')); + } + return value; +} -Status ResultRecovery::recover(const AnalysisModel& model, +bool EqualName(const std::string& left, const std::string& right) { + return left.size() == right.size() && + std::equal(left.begin(), left.end(), right.begin(), + [](const char left_value, const char right_value) { + return AsciiLower(left_value) == AsciiLower(right_value); + }); +} + +bool TryPositiveInteger(const std::string& text, std::int64_t& value) { + const char* const first = text.data(); + const char* const last = first + text.size(); + const auto parsed = std::from_chars(first, last, value); + return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; +} + +Result> ResolveLoadTarget(const Domain& domain, + const NodalLoad& load) { + std::vector sets; + for (const auto& set : domain.NodeSets()) { + if (EqualName(set.name, load.target)) { + sets.push_back(&set); + } + } + std::vector nodes; + std::int64_t source_label = 0; + if (TryPositiveInteger(load.target, source_label)) { + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + if (domain.Nodes()[node].source_id.source_label == source_label) { + nodes.push_back(static_cast(node)); + } + } + } + if (sets.size() > 1U || nodes.size() > 1U || + (!sets.empty() && !nodes.empty())) { + return RecoveryResultFailure>( + "invalid-node-station-entity", load.location, load.target, + "A station-eligibility load target must resolve unambiguously."); + } + if (!sets.empty()) { + std::vector seen(domain.Nodes().size(), 0U); + for (const EntityIndex node : sets.front()->node_indices) { + if (node >= domain.Nodes().size() || seen[node] != 0U) { + return RecoveryResultFailure>( + "invalid-node-station-entity", load.location, load.target, + "A station-eligibility node set must contain unique valid nodes."); + } + seen[node] = 1U; + } + return Result>::Success( + sets.front()->node_indices); + } + if (!nodes.empty()) { + return Result>::Success(std::move(nodes)); + } + return RecoveryResultFailure>( + "invalid-node-station-entity", load.location, load.target, + "A station-eligibility load target must resolve to a node or node set."); +} + +std::array Cross(const std::array& left, + const std::array& right) { + return {left[1U] * right[2U] - left[2U] * right[1U], + left[2U] * right[0U] - left[0U] * right[2U], + left[0U] * right[1U] - left[1U] * right[0U]}; +} + +double Dot(const std::array& left, + const std::array& right) { + return left[0U] * right[0U] + left[1U] * right[1U] + left[2U] * right[2U]; +} + +double Norm(const std::array& value) { + return std::hypot(value[0U], value[1U], value[2U]); +} + +bool AccumulateVectorAndScale(std::array& total, double& scale, + const std::array& contribution) { + const double magnitude = Norm(contribution); + const double accumulated_scale = scale + magnitude; + if (!IsFinite(contribution) || !std::isfinite(magnitude) || + !std::isfinite(accumulated_scale)) { + return false; + } + for (std::size_t component = 0U; component < contribution.size(); + ++component) { + const double accumulated = total[component] + contribution[component]; + if (!std::isfinite(accumulated)) { + return false; + } + total[component] = accumulated; + } + scale = accumulated_scale; + return true; +} + +double NormalizedBalance(const std::array& balance, + const double scale) { + const double balance_norm = Norm(balance); + if (!std::isfinite(balance_norm) || !std::isfinite(scale)) { + return (std::numeric_limits::infinity)(); + } + if (scale == 0.0) { + return balance_norm == 0.0 ? 0.0 + : (std::numeric_limits::infinity)(); + } + return balance_norm / scale; +} + +/// @brief Computes global shell evidence in stable source order. +Status PopulateShellGlobalEvidence(const Domain& domain, const DofManager& dofs, + const Vector& external_force, + const Vector& residual, + const double normalized_residual, + ShellStateCandidate& candidate) { + std::vector constrained(dofs.FullDofCount(), 0U); + for (const std::size_t full_dof : dofs.ConstrainedDofs()) { + constrained[full_dof] = 1U; + } + + std::array applied_force{}; + std::array reaction_force{}; + std::array applied_moment{}; + std::array reaction_moment{}; + double applied_force_scale = 0.0; + double reaction_force_scale = 0.0; + double applied_moment_scale = 0.0; + double reaction_moment_scale = 0.0; + for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { + const std::size_t offset = node * kDofsPerNode; + std::array nodal_applied_force{}; + std::array nodal_reaction_force{}; + std::array nodal_applied_moment{}; + std::array nodal_reaction_moment{}; + for (std::size_t component = 0U; component < 3U; ++component) { + nodal_applied_force[component] = external_force[offset + component]; + nodal_applied_moment[component] = external_force[offset + 3U + component]; + if (constrained[offset + component] != 0U) { + nodal_reaction_force[component] = residual[offset + component]; + } + if (constrained[offset + 3U + component] != 0U) { + nodal_reaction_moment[component] = residual[offset + 3U + component]; + } + } + + const auto applied_force_moment = + Cross(domain.Nodes()[node].coordinates, nodal_applied_force); + const auto reaction_force_moment = + Cross(domain.Nodes()[node].coordinates, nodal_reaction_force); + for (std::size_t component = 0U; component < 3U; ++component) { + nodal_applied_moment[component] += applied_force_moment[component]; + nodal_reaction_moment[component] += reaction_force_moment[component]; + } + if (!AccumulateVectorAndScale(applied_force, applied_force_scale, + nodal_applied_force) || + !AccumulateVectorAndScale(reaction_force, reaction_force_scale, + nodal_reaction_force) || + !AccumulateVectorAndScale(applied_moment, applied_moment_scale, + nodal_applied_moment) || + !AccumulateVectorAndScale(reaction_moment, reaction_moment_scale, + nodal_reaction_moment)) { + return RecoveryFailure("nonfinite-recovery-value", + domain.Nodes()[node].location, + domain.Nodes()[node].source_id.source_label_text, + "Global force and moment evidence must remain " + "finite in source-node order."); + } + } + + std::array force_balance{}; + std::array moment_balance{}; + for (std::size_t component = 0U; component < 3U; ++component) { + force_balance[component] = + applied_force[component] + reaction_force[component]; + moment_balance[component] = + applied_moment[component] + reaction_moment[component]; + candidate.equilibrium[component] = force_balance[component]; + candidate.equilibrium[3U + component] = moment_balance[component]; + } + const double force_metric = NormalizedBalance( + force_balance, (std::max)(applied_force_scale, reaction_force_scale)); + const double moment_metric = NormalizedBalance( + moment_balance, (std::max)(applied_moment_scale, reaction_moment_scale)); + candidate.verification_metrics = {normalized_residual, force_metric, + moment_metric}; + if (!IsFinite(candidate.equilibrium) || + !IsFinite(candidate.verification_metrics)) { + return RecoveryFailure("nonfinite-recovery-value", + {domain.SourcePath(), 0U}, "global-equilibrium", + "Global equilibrium values and their physical " + "normalization scales must be finite."); + } + if (force_metric > kGlobalEquilibriumTolerance || + moment_metric > kGlobalEquilibriumTolerance) { + return RecoveryFailure( + "global-equilibrium-tolerance-failure", {domain.SourcePath(), 0U}, + "global-equilibrium", + "Normalized global force or moment balance exceeds 1e-10."); + } + return Status::Ok(); +} + +std::optional LocalAxes(const Domain& domain, + const EulerBeam3DDefinition& element) { + const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; + const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; + const std::array delta = { + second[0U] - first[0U], second[1U] - first[1U], second[2U] - first[2U]}; + const double length = Norm(delta); + if (!std::isfinite(length) || !(length > 0.0)) { + return std::nullopt; + } + const std::array ex = {delta[0U] / length, delta[1U] / length, + delta[2U] / length}; + const auto& guide = domain.Sections()[element.section_index].first_axis; + const double projection = Dot(guide, ex); + const std::array ey_trial = {guide[0U] - projection * ex[0U], + guide[1U] - projection * ex[1U], + guide[2U] - projection * ex[2U]}; + const double ey_norm = Norm(ey_trial); + if (!std::isfinite(ey_norm) || !(ey_norm > 0.0)) { + return std::nullopt; + } + const std::array ey = { + ey_trial[0U] / ey_norm, ey_trial[1U] / ey_norm, ey_trial[2U] / ey_norm}; + const std::array ez = Cross(ex, ey); + const AxisSet axes = {ex, ey, ez}; + for (const auto& axis : axes) { + for (const double component : axis) { + if (!std::isfinite(component)) { + return std::nullopt; + } + } + } + return axes; +} + +bool SameAxes(const AxisSet& left, const AxisSet& right) { + for (std::size_t axis = 0U; axis < left.size(); ++axis) { + for (std::size_t component = 0U; component < left[axis].size(); + ++component) { + if (std::abs(left[axis][component] - right[axis][component]) > + kAxisTolerance) { + return false; + } + } + } + return true; +} + +} // namespace + +Status ResultRecovery::Recover(const AnalysisModel& model, const DofManager& dofs, - const SparseMatrix& fullStiffness, + const SparseMatrix& full_stiffness, AnalysisState& state) { - const Status inputStatus = - validateRecoveryInputs(model, dofs, fullStiffness, state); - if (!inputStatus.IsOk()) { - return inputStatus; + const Status input_status = + ValidateRecoveryInputs(model, dofs, full_stiffness, state); + if (!input_status.IsOk()) { + return input_status; + } + + Vector internal_force = full_stiffness.Multiply(state.Displacement()); + if (!IsFinite(internal_force)) { + return RecoveryFailure( + "nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U}, + model.GetDomain().SourceContentIdentity(), + "Full stiffness multiplication must produce finite internal force."); + } + Vector residual{dofs.FullDofCount()}; + for (std::size_t full_dof = 0U; full_dof < residual.Size(); ++full_dof) { + residual[full_dof] = + internal_force[full_dof] - state.ExternalForce()[full_dof]; + if (!std::isfinite(residual[full_dof])) { + return RecoveryFailure( + "nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U}, + std::to_string(full_dof), + "Internal-minus-external residual must remain finite."); + } + } + + const double residual_norm = IndexedNorm(residual, dofs.FreeDofs()); + const auto internal_term_norms = + FreeEquationInternalTermNorms(full_stiffness, state.Displacement(), dofs); + const double external_norm = + IndexedNorm(state.ExternalForce(), dofs.FreeDofs()); + // Normalize against the three terms of + // Kff*df + Kfc*dc - Ff. Using only the already-cancelled K*d term would + // classify prescribed-only equilibrium roundoff as a unit residual. + const double denominator = + (std::max)({internal_term_norms[0U], internal_term_norms[1U], + external_norm}); + if (!std::isfinite(residual_norm) || !std::isfinite(denominator)) { + return RecoveryFailure( + "nonfinite-recovery-value", {model.GetDomain().SourcePath(), 0U}, + "free-residual", + "Free residual and its physical normalization scale must be finite."); + } + // An exact zero-load equilibrium is well-defined as zero. No unit floor is + // introduced; a nonzero residual with zero physical scale fails closed. + const double normalized_residual = + denominator == 0.0 + ? (residual_norm == 0.0 ? 0.0 + : (std::numeric_limits::infinity)()) + : residual_norm / denominator; + if (!std::isfinite(normalized_residual) || + normalized_residual > kFreeResidualTolerance) { + return RecoveryFailure( + "free-residual-tolerance-failure", {model.GetDomain().SourcePath(), 0U}, + "free-residual", "The normalized free residual exceeds 1e-10."); + } + + // The full-space reaction dataset preserves free residual evidence while + // its constrained entries are the physical reactions from K*d-F. Element + // end actions remain distinct output and are never re-summed here. + Vector reaction = residual; + + std::vector endpoint_rows; + std::vector gauss_rows; + std::vector stress_rows; + endpoint_rows.reserve(model.ActiveElements().size() * 2U); + gauss_rows.reserve(model.ActiveElements().size() * 2U); + const Domain& domain = model.GetDomain(); + for (const EntityIndex element_index : model.ActiveElements()) { + const auto& definition = domain.Elements()[element_index]; + auto beam = + EulerBeam3D::Create(domain.Nodes()[definition.node_indices[0U]], + domain.Nodes()[definition.node_indices[1U]], + domain.Sections()[definition.section_index], + domain.Materials()[definition.material_index]); + if (!beam.HasValue()) { + return beam.GetStatus(); } - Vector internalForce = fullStiffness.Multiply(state.displacement()); - if (!finite(internalForce)) { - return recoveryFailure( - "nonfinite-recovery-value", - {model.domain().SourcePath(), 0U}, - model.domain().SourceContentIdentity(), - "Full stiffness multiplication must produce finite internal force."); + Vector element_displacement{kElementDofCount}; + const auto& scatter = dofs.ElementScatter(element_index); + for (std::size_t local_dof = 0U; local_dof < kElementDofCount; + ++local_dof) { + element_displacement[local_dof] = + state.Displacement()[scatter[local_dof]]; } - Vector residual{dofs.fullDofCount()}; - for (std::size_t fullDof = 0U; fullDof < residual.Size(); ++fullDof) { - residual[fullDof] = - internalForce[fullDof] - state.externalForce()[fullDof]; - if (!std::isfinite(residual[fullDof])) { - return recoveryFailure( - "nonfinite-recovery-value", - {model.domain().SourcePath(), 0U}, - std::to_string(fullDof), - "Internal-minus-external residual must remain finite."); - } + const BeamRecovery recovered = beam.Value().Recover(element_displacement); + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + if (!IsFinite(recovered.equilibrium_end_actions[endpoint]) || + !IsFinite(recovered.endpoint_section_resultants[endpoint])) { + return RecoveryFailure("nonfinite-recovery-value", definition.location, + definition.source_id.source_label_text, + "Endpoint recovery values must be finite."); + } + endpoint_rows.push_back( + {element_index, static_cast(endpoint), + domain.Nodes()[definition.node_indices[endpoint]].source_id, + recovered.equilibrium_end_actions[endpoint], + recovered.endpoint_section_resultants[endpoint]}); + } + for (std::size_t gauss = 0U; gauss < 2U; ++gauss) { + if (!IsFinite(recovered.gauss_generalized_strains[gauss]) || + !IsFinite(recovered.gauss_generalized_resultants[gauss])) { + return RecoveryFailure("nonfinite-recovery-value", definition.location, + definition.source_id.source_label_text, + "Gauss recovery values must be finite."); + } + gauss_rows.push_back({element_index, static_cast(gauss + 1U), + recovered.gauss_generalized_strains[gauss], + recovered.gauss_generalized_resultants[gauss]}); + } + for (const auto& point : recovered.stress_points) { + if ((point.gauss_point != 1 && point.gauss_point != 2) || + !std::isfinite(point.x1) || !std::isfinite(point.x2) || + !std::isfinite(point.s11)) { + return RecoveryFailure( + "nonfinite-recovery-value", definition.location, + definition.source_id.source_label_text, + "Stress recovery identity and values must be finite and ordered."); + } + stress_rows.push_back({element_index, point.gauss_point, + point.section_point, point.x1, point.x2, point.s11, + point.source}); + } + } + + ShellStateCandidate shell_candidate{}; + std::vector expected_shell_elements; + if (!domain.ShellElements().empty()) { + if (domain.ShellElements().size() > + (std::numeric_limits::max)() / kShellLocationCount) { + return RecoveryFailure( + "invalid-recovery-dimensions", {domain.SourcePath(), 0U}, + domain.SourceContentIdentity(), + "The shell result-row inventory exceeds the addressable range."); + } + std::vector>> directors_by_node( + domain.Nodes().size()); + for (const auto& frame : domain.ShellNodeInitialFrames()) { + if (frame.node_index >= directors_by_node.size() || + directors_by_node[frame.node_index].has_value()) { + return RecoveryFailure( + "invalid-recovery-entity", {domain.SourcePath(), 0U}, + std::to_string(frame.node_index), + "Shell initial directors must map uniquely to model nodes."); + } + directors_by_node[frame.node_index] = frame.director; } - const double residualNorm = indexedNorm(residual, dofs.freeDofs()); - const auto internalTermNorms = freeEquationInternalTermNorms( - fullStiffness, state.displacement(), dofs); - const double externalNorm = - indexedNorm(state.externalForce(), dofs.freeDofs()); - // Normalize against the three terms of - // Kff*df + Kfc*dc - Ff. Using only the already-cancelled K*d term would - // classify prescribed-only equilibrium roundoff as a unit residual. - const double denominator = (std::max)({ - internalTermNorms[0U], internalTermNorms[1U], externalNorm}); - if (!std::isfinite(residualNorm) || !std::isfinite(denominator)) { - return recoveryFailure( - "nonfinite-recovery-value", - {model.domain().SourcePath(), 0U}, - "free-residual", - "Free residual and its physical normalization scale must be finite."); + shell_candidate.rows.reserve(domain.ShellElements().size() * + kShellLocationCount); + expected_shell_elements.reserve(domain.ShellElements().size()); + constexpr std::array + locations{ShellMidsurfaceLocation::kGp1, ShellMidsurfaceLocation::kGp2, + ShellMidsurfaceLocation::kGp3, ShellMidsurfaceLocation::kGp4}; + constexpr std::array positions{ + ShellSectionPosition::kBottom, ShellSectionPosition::kMiddle, + ShellSectionPosition::kTop}; + constexpr std::array zeta{-1.0, 0.0, 1.0}; + for (std::size_t element_order = 0U; + element_order < domain.ShellElements().size(); ++element_order) { + const EntityIndex element_index = static_cast(element_order); + const auto& definition = domain.ShellElements()[element_order]; + std::array nodes{}; + std::array, 4> directors{}; + for (std::size_t node_position = 0U; + node_position < definition.node_indices.size(); ++node_position) { + const EntityIndex node = definition.node_indices[node_position]; + if (!directors_by_node[node].has_value()) { + return RecoveryFailure( + "invalid-recovery-entity", definition.location, + definition.source_id.source_label_text, + "Shell recovery requires one initial director per element node."); + } + nodes[node_position] = &domain.Nodes()[node]; + directors[node_position] = *directors_by_node[node]; + } + + auto shell = Mitc4Shell::Create( + nodes, directors, domain.ShellSections()[definition.section_index], + domain.Materials()[definition.material_index]); + if (!shell.HasValue()) { + return shell.GetStatus(); + } + Vector element_displacement{kShellElementDofCount}; + const auto& scatter = dofs.ShellElementScatter(element_index); + for (std::size_t local_dof = 0U; local_dof < kShellElementDofCount; + ++local_dof) { + element_displacement[local_dof] = + state.Displacement()[scatter[local_dof]]; + } + auto recovered = shell.Value().RecoverPhysical(element_displacement); + if (!recovered.HasValue()) { + return recovered.GetStatus(); + } + const double accumulated_energy = shell_candidate.physical_strain_energy + + recovered.Value().strain_energy; + if (!std::isfinite(accumulated_energy)) { + return RecoveryFailure( + "nonfinite-recovery-value", definition.location, + definition.source_id.source_label_text, + "Source-order physical shell energy reduction must remain finite."); + } + shell_candidate.physical_strain_energy = accumulated_energy; + expected_shell_elements.push_back(element_index); + + for (std::size_t point = 0U; point < recovered.Value().points.size(); + ++point) { + const auto& physical_point = recovered.Value().points[point]; + ShellResultRow row{}; + row.element = element_index; + row.location = locations[point]; + row.natural_coordinates = physical_point.natural_coordinates; + row.local_frame = {physical_point.local_frame.e1, + physical_point.local_frame.e2, + physical_point.local_frame.e3}; + row.generalized_strain = physical_point.generalized_strain; + row.section_resultant = physical_point.section_resultant; + for (std::size_t position = 0U; position < positions.size(); + ++position) { + row.stress[position] = {positions[position], zeta[position], + physical_point.in_plane_stress[position]}; + } + shell_candidate.rows.push_back(std::move(row)); + } } - // An exact zero-load equilibrium is well-defined as zero. No unit floor is - // introduced; a nonzero residual with zero physical scale fails closed. - const double normalizedResidual = denominator == 0.0 - ? (residualNorm == 0.0 - ? 0.0 - : (std::numeric_limits::infinity)()) - : residualNorm / denominator; - if (!std::isfinite(normalizedResidual) || - normalizedResidual > kFreeResidualTolerance) { - return recoveryFailure( - "free-residual-tolerance-failure", - {model.domain().SourcePath(), 0U}, - "free-residual", - "The normalized free residual exceeds 1e-10."); + const Status evidence_status = PopulateShellGlobalEvidence( + domain, dofs, state.ExternalForce(), residual, normalized_residual, + shell_candidate); + if (!evidence_status.IsOk()) { + return evidence_status; } + } - // The full-space reaction dataset preserves free residual evidence while - // its constrained entries are the physical reactions from K*d-F. Element - // end actions remain distinct output and are never re-summed here. - Vector reaction = residual; - - std::vector endpointRows; - std::vector gaussRows; - std::vector stressRows; - endpointRows.reserve(model.activeElements().size() * 2U); - gaussRows.reserve(model.activeElements().size() * 2U); - const Domain& domain = model.domain(); - for (const EntityIndex elementIndex : model.activeElements()) { - const auto& definition = domain.Elements()[elementIndex]; - auto beam = EulerBeam3D::Create( - domain.Nodes()[definition.node_indices[0U]], - domain.Nodes()[definition.node_indices[1U]], - domain.Sections()[definition.section_index], - domain.Materials()[definition.material_index]); - if (!beam.HasValue()) { - return beam.GetStatus(); - } - - Vector elementDisplacement{kElementDofCount}; - const auto& scatter = dofs.elementScatter(elementIndex); - for (std::size_t localDof = 0U; - localDof < kElementDofCount; - ++localDof) { - elementDisplacement[localDof] = - state.displacement()[scatter[localDof]]; - } - const BeamRecovery recovered = - beam.Value().Recover(elementDisplacement); - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - if (!finite(recovered.equilibrium_end_actions[endpoint]) || - !finite(recovered.endpoint_section_resultants[endpoint])) { - return recoveryFailure( - "nonfinite-recovery-value", - definition.location, - definition.source_id.source_label_text, - "Endpoint recovery values must be finite."); - } - endpointRows.push_back({ - elementIndex, - static_cast(endpoint), - domain.Nodes()[definition.node_indices[endpoint]].source_id, - recovered.equilibrium_end_actions[endpoint], - recovered.endpoint_section_resultants[endpoint]}); - } - for (std::size_t gauss = 0U; gauss < 2U; ++gauss) { - if (!finite(recovered.gauss_generalized_strains[gauss]) || - !finite(recovered.gauss_generalized_resultants[gauss])) { - return recoveryFailure( - "nonfinite-recovery-value", - definition.location, - definition.source_id.source_label_text, - "Gauss recovery values must be finite."); - } - gaussRows.push_back({ - elementIndex, - static_cast(gauss + 1U), - recovered.gauss_generalized_strains[gauss], - recovered.gauss_generalized_resultants[gauss]}); - } - for (const auto& point : recovered.stress_points) { - if ((point.gauss_point != 1 && point.gauss_point != 2) || - !std::isfinite(point.x1) || !std::isfinite(point.x2) || - !std::isfinite(point.s11)) { - return recoveryFailure( - "nonfinite-recovery-value", - definition.location, - definition.source_id.source_label_text, - "Stress recovery identity and values must be finite and ordered."); - } - stressRows.push_back({ - elementIndex, - point.gauss_point, - point.section_point, - point.x1, - point.x2, - point.s11, - point.source}); - } - } - - ShellStateCandidate shellCandidate{}; - std::vector expectedShellElements; - if (!domain.ShellElements().empty()) { - if (domain.ShellElements().size() > - (std::numeric_limits::max)() / - kShellLocationCount) { - return recoveryFailure( - "invalid-recovery-dimensions", - {domain.SourcePath(), 0U}, - domain.SourceContentIdentity(), - "The shell result-row inventory exceeds the addressable range."); - } - std::vector>> directorsByNode( - domain.Nodes().size()); - for (const auto& frame : domain.ShellNodeInitialFrames()) { - if (frame.node_index >= directorsByNode.size() || - directorsByNode[frame.node_index].has_value()) { - return recoveryFailure( - "invalid-recovery-entity", - {domain.SourcePath(), 0U}, - std::to_string(frame.node_index), - "Shell initial directors must map uniquely to model nodes."); - } - directorsByNode[frame.node_index] = frame.director; - } - - shellCandidate.rows.reserve( - domain.ShellElements().size() * kShellLocationCount); - expectedShellElements.reserve(domain.ShellElements().size()); - constexpr std::array - locations{ - ShellMidsurfaceLocation::gp1, - ShellMidsurfaceLocation::gp2, - ShellMidsurfaceLocation::gp3, - ShellMidsurfaceLocation::gp4}; - constexpr std::array positions{ - ShellSectionPosition::bottom, - ShellSectionPosition::middle, - ShellSectionPosition::top}; - constexpr std::array zeta{-1.0, 0.0, 1.0}; - for (std::size_t elementOrder = 0U; - elementOrder < domain.ShellElements().size(); - ++elementOrder) { - const EntityIndex elementIndex = - static_cast(elementOrder); - const auto& definition = domain.ShellElements()[elementOrder]; - std::array nodes{}; - std::array, 4> directors{}; - for (std::size_t nodePosition = 0U; - nodePosition < definition.node_indices.size(); - ++nodePosition) { - const EntityIndex node = definition.node_indices[nodePosition]; - if (!directorsByNode[node].has_value()) { - return recoveryFailure( - "invalid-recovery-entity", - definition.location, - definition.source_id.source_label_text, - "Shell recovery requires one initial director per element node."); - } - nodes[nodePosition] = &domain.Nodes()[node]; - directors[nodePosition] = *directorsByNode[node]; - } - - auto shell = Mitc4Shell::Create( - nodes, - directors, - domain.ShellSections()[definition.section_index], - domain.Materials()[definition.material_index]); - if (!shell.HasValue()) { - return shell.GetStatus(); - } - Vector elementDisplacement{kShellElementDofCount}; - const auto& scatter = dofs.shellElementScatter(elementIndex); - for (std::size_t localDof = 0U; - localDof < kShellElementDofCount; - ++localDof) { - elementDisplacement[localDof] = - state.displacement()[scatter[localDof]]; - } - auto recovered = shell.Value().RecoverPhysical( - elementDisplacement); - if (!recovered.HasValue()) { - return recovered.GetStatus(); - } - const double accumulatedEnergy = - shellCandidate.physicalStrainEnergy + - recovered.Value().strain_energy; - if (!std::isfinite(accumulatedEnergy)) { - return recoveryFailure( - "nonfinite-recovery-value", - definition.location, - definition.source_id.source_label_text, - "Source-order physical shell energy reduction must remain finite."); - } - shellCandidate.physicalStrainEnergy = accumulatedEnergy; - expectedShellElements.push_back(elementIndex); - - for (std::size_t point = 0U; - point < recovered.Value().points.size(); - ++point) { - const auto& physicalPoint = recovered.Value().points[point]; - ShellResultRow row{}; - row.element = elementIndex; - row.location = locations[point]; - row.naturalCoordinates = physicalPoint.natural_coordinates; - row.localFrame = { - physicalPoint.local_frame.e1, - physicalPoint.local_frame.e2, - physicalPoint.local_frame.e3}; - row.generalizedStrain = physicalPoint.generalized_strain; - row.sectionResultant = physicalPoint.section_resultant; - for (std::size_t position = 0U; - position < positions.size(); - ++position) { - row.stress[position] = { - positions[position], - zeta[position], - physicalPoint.in_plane_stress[position]}; - } - shellCandidate.rows.push_back(std::move(row)); - } - } - const Status evidenceStatus = populateShellGlobalEvidence( - domain, - dofs, - state.externalForce(), - residual, - normalizedResidual, - shellCandidate); - if (!evidenceStatus.IsOk()) { - return evidenceStatus; - } - } - - // Build and validate a complete candidate state first. This preserves the - // prior full residual, beam rows, and shell rows if any later shell or - // candidate-inventory validation fails. - AnalysisState candidateState = state; - candidateState.internalForce() = std::move(internalForce); - candidateState.residual() = std::move(residual); - candidateState.reaction() = std::move(reaction); - candidateState.endpointResults() = std::move(endpointRows); - candidateState.gaussResults() = std::move(gaussRows); - candidateState.stressResults() = std::move(stressRows); - const Status shellCommitStatus = candidateState.commitShellResults( - expectedShellElements, std::move(shellCandidate)); - if (!shellCommitStatus.IsOk()) { - return shellCommitStatus; - } - state = std::move(candidateState); - return Status::Ok(); + // Build and validate a complete candidate state first. This preserves the + // prior full residual, beam rows, and shell rows if any later shell or + // candidate-inventory validation fails. + AnalysisState candidate_state = state; + candidate_state.InternalForce() = std::move(internal_force); + candidate_state.Residual() = std::move(residual); + candidate_state.Reaction() = std::move(reaction); + candidate_state.EndpointResults() = std::move(endpoint_rows); + candidate_state.GaussResults() = std::move(gauss_rows); + candidate_state.StressResults() = std::move(stress_rows); + const Status shell_commit_status = candidate_state.CommitShellResults( + expected_shell_elements, std::move(shell_candidate)); + if (!shell_commit_status.IsOk()) { + return shell_commit_status; + } + state = std::move(candidate_state); + return Status::Ok(); } Result> -ResultRecovery::normalizeSectionResultantsToNodeStations( +ResultRecovery::NormalizeSectionResultantsToNodeStations( const AnalysisModel& model, - const std::vector& endpointRows, - const std::array& componentTolerances) { - const Domain& domain = model.domain(); - for (const double tolerance : componentTolerances) { - if (!std::isfinite(tolerance) || tolerance < 0.0) { - return recoveryResultFailure>( - "invalid-node-station-tolerance", - {domain.SourcePath(), 0U}, - "component-tolerances", - "Node-station component tolerances must be finite and nonnegative."); - } + const std::vector& endpoint_rows, + const std::array& component_tolerances) { + const Domain& domain = model.GetDomain(); + for (const double tolerance : component_tolerances) { + if (!std::isfinite(tolerance) || tolerance < 0.0) { + return RecoveryResultFailure>( + "invalid-node-station-tolerance", {domain.SourcePath(), 0U}, + "component-tolerances", + "Node-station component tolerances must be finite and nonnegative."); } - if (model.activeElements().size() > - (std::numeric_limits::max)() / 2U || - endpointRows.size() != model.activeElements().size() * 2U) { - return recoveryResultFailure>( - "invalid-node-station-shape", - {domain.SourcePath(), 0U}, - std::to_string(endpointRows.size()), - "Endpoint rows must contain exactly two rows per active element."); + } + if (model.ActiveElements().size() > + (std::numeric_limits::max)() / 2U || + endpoint_rows.size() != model.ActiveElements().size() * 2U) { + return RecoveryResultFailure>( + "invalid-node-station-shape", {domain.SourcePath(), 0U}, + std::to_string(endpoint_rows.size()), + "Endpoint rows must contain exactly two rows per active element."); + } + + std::vector> rows_by_node( + domain.Nodes().size()); + for (std::size_t order = 0U; order < model.ActiveElements().size(); ++order) { + const EntityIndex element_index = model.ActiveElements()[order]; + if (element_index >= domain.Elements().size()) { + return RecoveryResultFailure>( + "invalid-node-station-entity", {domain.SourcePath(), 0U}, + std::to_string(element_index), + "Every active station element must be a valid stable entity."); + } + const auto& definition = domain.Elements()[element_index]; + for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { + const auto& row = endpoint_rows[order * 2U + endpoint]; + const EntityIndex node_index = definition.node_indices[endpoint]; + if (node_index >= domain.Nodes().size() || row.element != element_index || + row.endpoint != static_cast(endpoint) || + !SameSourceIdentity(row.node, domain.Nodes()[node_index].source_id)) { + return RecoveryResultFailure>( + "invalid-node-station-entity", definition.location, + definition.source_id.source_label_text, + "Endpoint rows must preserve active element, endpoint, and " + "source-node order."); + } + if (!IsFinite(row.section_resultant)) { + return RecoveryResultFailure>( + "nonfinite-node-station-value", definition.location, + definition.source_id.source_label_text, + "Node-station section resultants must be finite."); + } + rows_by_node[node_index].push_back(&row); + } + } + + std::vector loaded_nodes(domain.Nodes().size(), 0U); + if (model.ActiveLoads().size() != model.Step().loads.size()) { + return RecoveryResultFailure>( + "invalid-node-station-entity", model.Step().location, model.Step().name, + "The active load view must preserve every sole-step load."); + } + for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) { + const EntityIndex load_index = model.ActiveLoads()[order]; + if (load_index != order || load_index >= model.Step().loads.size()) { + return RecoveryResultFailure>( + "invalid-node-station-entity", model.Step().location, + std::to_string(load_index), + "Active loads must remain in stable source order."); + } + const auto& load = model.Step().loads[load_index]; + if (!std::isfinite(load.magnitude)) { + return RecoveryResultFailure>( + "nonfinite-node-station-value", load.location, load.target, + "Station eligibility requires finite concentrated loads."); + } + auto targets = ResolveLoadTarget(domain, load); + if (!targets.HasValue()) { + return Result>::Failure( + targets.GetStatus()); + } + if (load.magnitude != 0.0) { + for (const EntityIndex node : targets.Value()) { + loaded_nodes[node] = 1U; + } + } + } + + std::vector stations; + stations.reserve(domain.Nodes().size()); + for (std::size_t node_index = 0U; node_index < rows_by_node.size(); + ++node_index) { + const auto& incident = rows_by_node[node_index]; + if (incident.empty()) { + continue; + } + if (incident.size() == 1U) { + stations.push_back({domain.Nodes()[node_index].source_id, + incident.front()->element, + incident.front()->section_resultant}); + continue; + } + if (incident.size() != 2U || loaded_nodes[node_index] != 0U) { + return RecoveryResultFailure>( + "ineligible-node-station", domain.Nodes()[node_index].location, + domain.Nodes()[node_index].source_id.source_label_text, + "Interior station collapse requires exactly two unloaded endpoints."); } - std::vector> rowsByNode( - domain.Nodes().size()); - for (std::size_t order = 0U; - order < model.activeElements().size(); - ++order) { - const EntityIndex elementIndex = model.activeElements()[order]; - if (elementIndex >= domain.Elements().size()) { - return recoveryResultFailure>( - "invalid-node-station-entity", - {domain.SourcePath(), 0U}, - std::to_string(elementIndex), - "Every active station element must be a valid stable entity."); - } - const auto& definition = domain.Elements()[elementIndex]; - for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { - const auto& row = endpointRows[order * 2U + endpoint]; - const EntityIndex nodeIndex = definition.node_indices[endpoint]; - if (nodeIndex >= domain.Nodes().size() || - row.element != elementIndex || - row.endpoint != static_cast(endpoint) || - !sameSourceIdentity(row.node, domain.Nodes()[nodeIndex].source_id)) { - return recoveryResultFailure>( - "invalid-node-station-entity", - definition.location, - definition.source_id.source_label_text, - "Endpoint rows must preserve active element, endpoint, and source-node order."); - } - if (!finite(row.sectionResultant)) { - return recoveryResultFailure>( - "nonfinite-node-station-value", - definition.location, - definition.source_id.source_label_text, - "Node-station section resultants must be finite."); - } - rowsByNode[nodeIndex].push_back(&row); - } + const auto& first_element = domain.Elements()[incident[0U]->element]; + const auto& second_element = domain.Elements()[incident[1U]->element]; + const bool chain_orientation = + incident[0U]->endpoint != incident[1U]->endpoint && + ((incident[0U]->endpoint == 1 && incident[1U]->endpoint == 0) || + (incident[0U]->endpoint == 0 && incident[1U]->endpoint == 1)); + const auto first_axes = LocalAxes(domain, first_element); + const auto second_axes = LocalAxes(domain, second_element); + if (!chain_orientation || + first_element.section_index != second_element.section_index || + !first_axes.has_value() || !second_axes.has_value() || + !SameAxes(*first_axes, *second_axes)) { + return RecoveryResultFailure>( + "ineligible-node-station", domain.Nodes()[node_index].location, + domain.Nodes()[node_index].source_id.source_label_text, + "Interior station endpoints require one consistent section and " + "local-axis chain."); } - std::vector loadedNodes(domain.Nodes().size(), 0U); - if (model.activeLoads().size() != model.step().loads.size()) { - return recoveryResultFailure>( - "invalid-node-station-entity", - model.step().location, - model.step().name, - "The active load view must preserve every sole-step load."); - } - for (std::size_t order = 0U; order < model.activeLoads().size(); ++order) { - const EntityIndex loadIndex = model.activeLoads()[order]; - if (loadIndex != order || loadIndex >= model.step().loads.size()) { - return recoveryResultFailure>( - "invalid-node-station-entity", - model.step().location, - std::to_string(loadIndex), - "Active loads must remain in stable source order."); - } - const auto& load = model.step().loads[loadIndex]; - if (!std::isfinite(load.magnitude)) { - return recoveryResultFailure>( - "nonfinite-node-station-value", - load.location, - load.target, - "Station eligibility requires finite concentrated loads."); - } - auto targets = resolveLoadTarget(domain, load); - if (!targets.HasValue()) { - return Result>::Failure( - targets.GetStatus()); - } - if (load.magnitude != 0.0) { - for (const EntityIndex node : targets.Value()) { - loadedNodes[node] = 1U; - } - } + // Endpoint section-resultant rows already use the positive-local-x cut + // convention. Once common orientation is proven, no outward-action + // endpoint sign is applied and the values are directly comparable. + for (std::size_t component = 0U; component < component_tolerances.size(); + ++component) { + const double difference = + std::abs(incident[0U]->section_resultant[component] - + incident[1U]->section_resultant[component]); + if (!std::isfinite(difference)) { + return RecoveryResultFailure>( + "nonfinite-node-station-value", domain.Nodes()[node_index].location, + domain.Nodes()[node_index].source_id.source_label_text, + "Endpoint comparison must produce a finite difference."); + } + if (difference > component_tolerances[component]) { + return RecoveryResultFailure>( + "node-station-tolerance-failure", + domain.Nodes()[node_index].location, + domain.Nodes()[node_index].source_id.source_label_text, + "Interior endpoint resultants disagree beyond component " + "tolerance."); + } } - std::vector stations; - stations.reserve(domain.Nodes().size()); - for (std::size_t nodeIndex = 0U; - nodeIndex < rowsByNode.size(); - ++nodeIndex) { - const auto& incident = rowsByNode[nodeIndex]; - if (incident.empty()) { - continue; - } - if (incident.size() == 1U) { - stations.push_back({ - domain.Nodes()[nodeIndex].source_id, - incident.front()->element, - incident.front()->sectionResultant}); - continue; - } - if (incident.size() != 2U || loadedNodes[nodeIndex] != 0U) { - return recoveryResultFailure>( - "ineligible-node-station", - domain.Nodes()[nodeIndex].location, - domain.Nodes()[nodeIndex].source_id.source_label_text, - "Interior station collapse requires exactly two unloaded endpoints."); - } - - const auto& firstElement = domain.Elements()[incident[0U]->element]; - const auto& secondElement = domain.Elements()[incident[1U]->element]; - const bool chainOrientation = - incident[0U]->endpoint != incident[1U]->endpoint && - ((incident[0U]->endpoint == 1 && incident[1U]->endpoint == 0) || - (incident[0U]->endpoint == 0 && incident[1U]->endpoint == 1)); - const auto firstAxes = localAxes(domain, firstElement); - const auto secondAxes = localAxes(domain, secondElement); - if (!chainOrientation || - firstElement.section_index != secondElement.section_index || - !firstAxes.has_value() || !secondAxes.has_value() || - !sameAxes(*firstAxes, *secondAxes)) { - return recoveryResultFailure>( - "ineligible-node-station", - domain.Nodes()[nodeIndex].location, - domain.Nodes()[nodeIndex].source_id.source_label_text, - "Interior station endpoints require one consistent section and local-axis chain."); - } - - // Endpoint sectionResultant rows already use the positive-local-x cut - // convention. Once common orientation is proven, no outward-action - // endpoint sign is applied and the values are directly comparable. - for (std::size_t component = 0U; - component < componentTolerances.size(); - ++component) { - const double difference = std::abs( - incident[0U]->sectionResultant[component] - - incident[1U]->sectionResultant[component]); - if (!std::isfinite(difference)) { - return recoveryResultFailure>( - "nonfinite-node-station-value", - domain.Nodes()[nodeIndex].location, - domain.Nodes()[nodeIndex].source_id.source_label_text, - "Endpoint comparison must produce a finite difference."); - } - if (difference > componentTolerances[component]) { - return recoveryResultFailure>( - "node-station-tolerance-failure", - domain.Nodes()[nodeIndex].location, - domain.Nodes()[nodeIndex].source_id.source_label_text, - "Interior endpoint resultants disagree beyond component tolerance."); - } - } - - const EndpointResultRow* representative = - incident[0U]->element < incident[1U]->element - ? incident[0U] - : incident[1U]; - stations.push_back({ - domain.Nodes()[nodeIndex].source_id, - representative->element, - representative->sectionResultant}); - } - return Result>::Success( - std::move(stations)); + const EndpointResultRow* representative = + incident[0U]->element < incident[1U]->element ? incident[0U] + : incident[1U]; + stations.push_back({domain.Nodes()[node_index].source_id, + representative->element, + representative->section_resultant}); + } + return Result>::Success( + std::move(stations)); } -} // namespace fesa +} // namespace fesa diff --git a/tests/integration/analysis/linear_static_analysis_test.cpp b/tests/integration/analysis/linear_static_analysis_test.cpp index 22ec87a..9cfeda4 100644 --- a/tests/integration/analysis/linear_static_analysis_test.cpp +++ b/tests/integration/analysis/linear_static_analysis_test.cpp @@ -1,8 +1,4 @@ -#include "fesa/analysis/linear_static_analysis.hpp" - -#include "fesa/assembly/parallel_for.hpp" -#include "fesa/results/results_writer.hpp" -#include "fesa/solvers/linear/mkl_pardiso_solver.h" +#include "fesa/analysis/linear_static_analysis.h" #include @@ -18,49 +14,52 @@ #include #include +#include "fesa/assembly/parallel_for.h" +#include "fesa/results/results_writer.h" +#include "fesa/solvers/linear/mkl_pardiso_solver.h" + namespace { class TempDirectory { -public: - explicit TempDirectory(const std::string& label) { - static std::atomic sequence{0U}; - const auto tick = std::chrono::steady_clock::now() - .time_since_epoch() - .count(); - path_ = std::filesystem::temp_directory_path() / - ("fesa-step24-analysis-" + label + "-" + - std::to_string(tick) + "-" + - std::to_string(sequence.fetch_add(1U))); - std::error_code error; - if (!std::filesystem::create_directory(path_, error) || error) { - throw std::runtime_error{"Unable to create the Step 24 analysis fixture."}; - } + public: + explicit TempDirectory(const std::string& label) { + static std::atomic sequence{0U}; + const auto tick = + std::chrono::steady_clock::now().time_since_epoch().count(); + path_ = std::filesystem::temp_directory_path() / + ("fesa-step24-analysis-" + label + "-" + std::to_string(tick) + + "-" + std::to_string(sequence.fetch_add(1U))); + std::error_code error; + if (!std::filesystem::create_directory(path_, error) || error) { + throw std::runtime_error{ + "Unable to create the Step 24 analysis fixture."}; } + } - TempDirectory(const TempDirectory&) = delete; - TempDirectory& operator=(const TempDirectory&) = delete; + TempDirectory(const TempDirectory&) = delete; + TempDirectory& operator=(const TempDirectory&) = delete; - ~TempDirectory() { - std::error_code ignored; - std::filesystem::remove_all(path_, ignored); - } + ~TempDirectory() { + std::error_code ignored; + std::filesystem::remove_all(path_, ignored); + } - const std::filesystem::path& path() const noexcept { return path_; } + const std::filesystem::path& Path() const noexcept { return path_; } -private: - std::filesystem::path path_; + private: + std::filesystem::path path_; }; -void writeText(const std::filesystem::path& path, const std::string& text) { - std::ofstream stream{path, std::ios::binary | std::ios::trunc}; - stream.write(text.data(), static_cast(text.size())); - if (!stream) { - throw std::runtime_error{"Unable to write the Step 24 analysis input."}; - } +void WriteText(const std::filesystem::path& path, const std::string& text) { + std::ofstream stream{path, std::ios::binary | std::ios::trunc}; + stream.write(text.data(), static_cast(text.size())); + if (!stream) { + throw std::runtime_error{"Unable to write the Step 24 analysis input."}; + } } -std::string axialDeck(const double rootUx, const double tipForce) { - return R"inp(*Part, name=BeamPart +std::string AxialDeck(const double root_ux, const double tip_force) { + return R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. 2, 2., 0., 0. @@ -84,22 +83,23 @@ std::string axialDeck(const double rootUx, const double tipForce) { *Elastic 100., 0.25 *Boundary -Root, 1, 1, )inp" + std::to_string(rootUx) + R"inp( +Root, 1, 1, )inp" + + std::to_string(root_ux) + R"inp( Root, 2, 6 Tip, 2, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload -Tip, 1, )inp" + std::to_string(tipForce) + R"inp( +Tip, 1, )inp" + + std::to_string(tip_force) + R"inp( *End Step )inp"; } -std::string shellDeck( - const std::string& boundaryBlock, - const std::string& loadBlock = {}) { - return std::string{R"inp(*Part, name=ShellPart +std::string ShellDeck(const std::string& boundary_block, + const std::string& load_block = {}) { + return std::string{R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. @@ -129,19 +129,21 @@ std::string shellDeck( *Material, name=Steel *Elastic 1000., 0.25 -)inp"} + boundaryBlock + R"inp(*Step, name=Load, nlgeom=NO +)inp"} + boundary_block + + R"inp(*Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. -)inp" + loadBlock + R"inp(*End Step +)inp" + load_block + + R"inp(*End Step )inp"; } -std::string allConstrainedShellDeck() { - return shellDeck("*Boundary\nAll, 1, 6\n"); +std::string AllConstrainedShellDeck() { + return ShellDeck("*Boundary\nAll, 1, 6\n"); } -std::string prescribedShellDeck() { - return shellDeck(R"inp(*Boundary +std::string PrescribedShellDeck() { + return ShellDeck(R"inp(*Boundary N1, 1, 6 N2, 1, 1, 0.1 N2, 2, 6 @@ -150,383 +152,373 @@ N4, 1, 6 )inp"); } -// The pure Template Method spy makes the eight public lifecycle hooks observable -// without coupling the ordering assertion to any solver backend. +// The pure Template Method spy makes the eight public lifecycle hooks +// observable without coupling the ordering assertion to any solver backend. class SpyAnalysis final : public fesa::Analysis { -public: - const std::vector& events() const noexcept { return events_; } + public: + const std::vector& Events() const noexcept { return events_; } -protected: - fesa::Status initialize(const fesa::AnalysisRequest&) override { - return record("initialize"); - } - fesa::Status buildAnalysisModel() override { - return record("build-analysis-model"); - } - fesa::Status buildDofMapAndSparsePattern() override { - return record("build-dof-map-and-sparse-pattern"); - } - fesa::Status assembleAndPartitionStiffness() override { - return record("assemble-and-partition-stiffness"); - } - fesa::Status factorize() override { return record("factorize"); } - fesa::Status assembleLoadsAndEffectiveRhs() override { - return record("assemble-loads-and-effective-rhs"); - } - fesa::Status substituteAndReconstruct() override { - return record("substitute-and-reconstruct"); - } - fesa::Status recoverAndWriteResults() override { - return record("recover-and-write-results"); - } + protected: + fesa::Status Initialize(const fesa::AnalysisRequest&) override { + return Record("initialize"); + } + fesa::Status BuildAnalysisModel() override { + return Record("build-analysis-model"); + } + fesa::Status BuildDofMapAndSparsePattern() override { + return Record("build-dof-map-and-sparse-pattern"); + } + fesa::Status AssembleAndPartitionStiffness() override { + return Record("assemble-and-partition-stiffness"); + } + fesa::Status Factorize() override { return Record("factorize"); } + fesa::Status AssembleLoadsAndEffectiveRhs() override { + return Record("assemble-loads-and-effective-rhs"); + } + fesa::Status SubstituteAndReconstruct() override { + return Record("substitute-and-reconstruct"); + } + fesa::Status RecoverAndWriteResults() override { + return Record("recover-and-write-results"); + } -private: - fesa::Status record(const char* event) { - events_.emplace_back(event); - return fesa::Status::Ok(); - } + private: + fesa::Status Record(const char* event) { + events_.emplace_back(event); + return fesa::Status::Ok(); + } - std::vector events_; + std::vector events_; }; // The solver spy records only the adapter-boundary operations. In particular, // solve() cannot conceal a second factorization call. class SpyLinearSolver final : public fesa::LinearSolver { -public: - explicit SpyLinearSolver(std::vector& events) - : events_{events} {} + public: + explicit SpyLinearSolver(std::vector& events) + : events_{events} {} - fesa::Status Factorize(const fesa::SparseMatrix&) override { - ++factorizeCalls_; - events_.emplace_back("solver-factorize"); - return fesa::Status::Ok(); + fesa::Status Factorize(const fesa::SparseMatrix&) override { + ++factorize_calls_; + events_.emplace_back("solver-factorize"); + return fesa::Status::Ok(); + } + + fesa::Status Solve(const fesa::Vector& rhs, + fesa::Vector& solution) const override { + ++solve_calls_; + events_.emplace_back("solver-solve"); + for (std::size_t index = 0U; index < rhs.Size() && index < solution.Size(); + ++index) { + solution[index] = 0.0; } + return fesa::Status::Ok(); + } - fesa::Status Solve( - const fesa::Vector& rhs, fesa::Vector& solution) const override { - ++solveCalls_; - events_.emplace_back("solver-solve"); - for (std::size_t index = 0U; - index < rhs.Size() && index < solution.Size(); ++index) { - solution[index] = 0.0; - } - return fesa::Status::Ok(); - } + int FactorizeCalls() const noexcept { return factorize_calls_; } + int SolveCalls() const noexcept { return solve_calls_; } - int factorizeCalls() const noexcept { return factorizeCalls_; } - int solveCalls() const noexcept { return solveCalls_; } - -private: - std::vector& events_; - int factorizeCalls_{0}; - mutable int solveCalls_{0}; + private: + std::vector& events_; + int factorize_calls_{0}; + mutable int solve_calls_{0}; }; class RecordingMklSolver final : public fesa::LinearSolver { -public: - fesa::Status Factorize(const fesa::SparseMatrix& matrix) override { - ++factorizeCalls_; - factorizedDimension_ = matrix.Rows(); - if (matrix.Rows() == 1U && matrix.Columns() == 1U && - matrix.Values().size() == 1U) { - scalarStiffness_ = matrix.Values()[0U]; - } - return backend_.Factorize(matrix); + public: + fesa::Status Factorize(const fesa::SparseMatrix& matrix) override { + ++factorize_calls_; + factorized_dimension_ = matrix.Rows(); + if (matrix.Rows() == 1U && matrix.Columns() == 1U && + matrix.Values().size() == 1U) { + scalar_stiffness_ = matrix.Values()[0U]; } + return backend_.Factorize(matrix); + } - fesa::Status Solve( - const fesa::Vector& rhs, fesa::Vector& solution) const override { - ++solveCalls_; - if (rhs.Size() == 0U) { - rhs_.clear(); - } else { - rhs_.assign(rhs.Data(), rhs.Data() + rhs.Size()); - } - return backend_.Solve(rhs, solution); + fesa::Status Solve(const fesa::Vector& rhs, + fesa::Vector& solution) const override { + ++solve_calls_; + if (rhs.Size() == 0U) { + rhs_.clear(); + } else { + rhs_.assign(rhs.Data(), rhs.Data() + rhs.Size()); } + return backend_.Solve(rhs, solution); + } - int factorizeCalls() const noexcept { return factorizeCalls_; } - int solveCalls() const noexcept { return solveCalls_; } - std::size_t factorizedDimension() const noexcept { - return factorizedDimension_; - } - double scalarStiffness() const noexcept { return scalarStiffness_; } - const std::vector& rhs() const noexcept { return rhs_; } + int FactorizeCalls() const noexcept { return factorize_calls_; } + int SolveCalls() const noexcept { return solve_calls_; } + std::size_t FactorizedDimension() const noexcept { + return factorized_dimension_; + } + double ScalarStiffness() const noexcept { return scalar_stiffness_; } + const std::vector& Rhs() const noexcept { return rhs_; } -private: - fesa::MklPardisoSolver backend_; - int factorizeCalls_{0}; - mutable int solveCalls_{0}; - std::size_t factorizedDimension_{0U}; - double scalarStiffness_{0.0}; - mutable std::vector rhs_; + private: + fesa::MklPardisoSolver backend_; + int factorize_calls_{0}; + mutable int solve_calls_{0}; + std::size_t factorized_dimension_{0U}; + double scalar_stiffness_{0.0}; + mutable std::vector rhs_; }; class NonfiniteLinearSolver final : public fesa::LinearSolver { -public: - fesa::Status Factorize(const fesa::SparseMatrix&) override { - return fesa::Status::Ok(); - } + public: + fesa::Status Factorize(const fesa::SparseMatrix&) override { + return fesa::Status::Ok(); + } - fesa::Status Solve( - const fesa::Vector&, fesa::Vector& solution) const override { - for (std::size_t index = 0U; index < solution.Size(); ++index) { - solution[index] = (std::numeric_limits::quiet_NaN)(); - } - return fesa::Status::Ok(); + fesa::Status Solve(const fesa::Vector&, + fesa::Vector& solution) const override { + for (std::size_t index = 0U; index < solution.Size(); ++index) { + solution[index] = (std::numeric_limits::quiet_NaN)(); } + return fesa::Status::Ok(); + } }; class SpyResultsWriter final : public fesa::ResultsWriter { -public: - explicit SpyResultsWriter(std::vector& events) - : events_{events} {} + public: + explicit SpyResultsWriter(std::vector& events) + : events_{events} {} - fesa::Status write( - const std::filesystem::path&, - const fesa::Domain&, - const fesa::AnalysisState&, - const std::vector&) override { - ++writeCalls_; - events_.emplace_back("writer-write"); - return fesa::Status::Ok(); - } + fesa::Status Write(const std::filesystem::path&, const fesa::Domain&, + const fesa::AnalysisState&, + const std::vector&) override { + ++write_calls_; + events_.emplace_back("writer-write"); + return fesa::Status::Ok(); + } - int writeCalls() const noexcept { return writeCalls_; } + int WriteCalls() const noexcept { return write_calls_; } -private: - std::vector& events_; - int writeCalls_{0}; + private: + std::vector& events_; + int write_calls_{0}; }; class CapturingResultsWriter final : public fesa::ResultsWriter { -public: - fesa::Status write( - const std::filesystem::path& outputPath, - const fesa::Domain& domain, - const fesa::AnalysisState& state, - const std::vector& diagnostics) override { - outputPath_ = outputPath; - nodeCount_ = domain.Nodes().size(); - shellElementCount_ = domain.ShellElements().size(); - state_ = std::make_unique(state); - diagnostics_ = diagnostics; - return fesa::Status::Ok(); - } + public: + fesa::Status Write( + const std::filesystem::path& output_path, const fesa::Domain& domain, + const fesa::AnalysisState& state, + const std::vector& diagnostics) override { + output_path_ = output_path; + node_count_ = domain.Nodes().size(); + shell_element_count_ = domain.ShellElements().size(); + state_ = std::make_unique(state); + diagnostics_ = diagnostics; + return fesa::Status::Ok(); + } - const fesa::AnalysisState& state() const { - if (!state_) { - throw std::logic_error{"No AnalysisState was captured."}; - } - return *state_; + const fesa::AnalysisState& State() const { + if (!state_) { + throw std::logic_error{"No AnalysisState was captured."}; } + return *state_; + } - const std::filesystem::path& outputPath() const noexcept { - return outputPath_; - } - std::size_t nodeCount() const noexcept { return nodeCount_; } - std::size_t shellElementCount() const noexcept { - return shellElementCount_; - } - const std::vector& diagnostics() const noexcept { - return diagnostics_; - } + const std::filesystem::path& OutputPath() const noexcept { + return output_path_; + } + std::size_t NodeCount() const noexcept { return node_count_; } + std::size_t ShellElementCount() const noexcept { + return shell_element_count_; + } + const std::vector& Diagnostics() const noexcept { + return diagnostics_; + } -private: - std::filesystem::path outputPath_; - std::size_t nodeCount_{0U}; - std::size_t shellElementCount_{0U}; - std::unique_ptr state_; - std::vector diagnostics_; + private: + std::filesystem::path output_path_; + std::size_t node_count_{0U}; + std::size_t shell_element_count_{0U}; + std::unique_ptr state_; + std::vector diagnostics_; }; -} // namespace +} // namespace TEST(LinearStaticCli, FactorizesBeforeLoadAndSolvesWithoutRefactorization) { - SpyAnalysis lifecycle; - const fesa::AnalysisRequest emptyRequest{}; - ASSERT_TRUE(lifecycle.run(emptyRequest).IsOk()); - EXPECT_EQ( - lifecycle.events(), - (std::vector{ - "initialize", - "build-analysis-model", - "build-dof-map-and-sparse-pattern", - "assemble-and-partition-stiffness", - "factorize", - "assemble-loads-and-effective-rhs", - "substitute-and-reconstruct", - "recover-and-write-results"})); + SpyAnalysis lifecycle; + const fesa::AnalysisRequest empty_request{}; + ASSERT_TRUE(lifecycle.Run(empty_request).IsOk()); + EXPECT_EQ(lifecycle.Events(), + (std::vector{ + "initialize", "build-analysis-model", + "build-dof-map-and-sparse-pattern", + "assemble-and-partition-stiffness", "factorize", + "assemble-loads-and-effective-rhs", + "substitute-and-reconstruct", "recover-and-write-results"})); - TempDirectory directory{"order"}; - const auto input = directory.path() / "order.inp"; - const auto output = directory.path() / "results.h5"; - writeText(input, axialDeck(0.0, 0.0)); + TempDirectory directory{"order"}; + const auto input = directory.Path() / "order.inp"; + const auto output = directory.Path() / "results.h5"; + WriteText(input, AxialDeck(0.0, 0.0)); - std::vector adapterEvents; - fesa::SerialParallelFor serial; - SpyLinearSolver solver{adapterEvents}; - SpyResultsWriter writer{adapterEvents}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::vector adapter_events; + fesa::SerialParallelFor serial; + SpyLinearSolver solver{adapter_events}; + SpyResultsWriter writer{adapter_events}; + fesa::LinearStaticAnalysis analysis{serial, solver, writer}; - ASSERT_TRUE(analysis.run({input, output}).IsOk()); - EXPECT_EQ(solver.factorizeCalls(), 1); - EXPECT_EQ(solver.solveCalls(), 1); - EXPECT_EQ(writer.writeCalls(), 1); - EXPECT_EQ( - adapterEvents, - (std::vector{ - "solver-factorize", "solver-solve", "writer-write"})); + ASSERT_TRUE(analysis.Run({input, output}).IsOk()); + EXPECT_EQ(solver.FactorizeCalls(), 1); + EXPECT_EQ(solver.SolveCalls(), 1); + EXPECT_EQ(writer.WriteCalls(), 1); + EXPECT_EQ(adapter_events, + (std::vector{"solver-factorize", "solver-solve", + "writer-write"})); } TEST(LinearStaticCli, RealPipelineHandlesAnalyticalAndNonzeroPrescription) { - TempDirectory directory{"analytical"}; - const auto input = directory.path() / "prescribed-axial.inp"; - const auto output = directory.path() / "captured-results.h5"; - writeText(input, axialDeck(0.1, 10.0)); + TempDirectory directory{"analytical"}; + const auto input = directory.Path() / "prescribed-axial.inp"; + const auto output = directory.Path() / "captured-results.h5"; + WriteText(input, AxialDeck(0.1, 10.0)); - fesa::SerialParallelFor serial; - fesa::MklPardisoSolver solver; - CapturingResultsWriter writer; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + fesa::SerialParallelFor serial; + fesa::MklPardisoSolver solver; + CapturingResultsWriter writer; + fesa::LinearStaticAnalysis analysis{serial, solver, writer}; - const auto status = analysis.run({input, output}); - ASSERT_TRUE(status.IsOk()); - EXPECT_EQ(writer.outputPath(), output); - EXPECT_EQ(writer.nodeCount(), 2U); - EXPECT_TRUE(writer.diagnostics().empty()); + const auto status = analysis.Run({input, output}); + ASSERT_TRUE(status.IsOk()); + EXPECT_EQ(writer.OutputPath(), output); + EXPECT_EQ(writer.NodeCount(), 2U); + EXPECT_TRUE(writer.Diagnostics().empty()); - const auto& state = writer.state(); - ASSERT_EQ(state.displacement().Size(), 12U); - EXPECT_EQ(state.identity().stepName, "Step-1"); - EXPECT_EQ(state.identity().frameIndex, 0U); + const auto& state = writer.State(); + ASSERT_EQ(state.Displacement().Size(), 12U); + EXPECT_EQ(state.Identity().step_name, "Step-1"); + EXPECT_EQ(state.Identity().frame_index, 0U); - // EA/L = 100 for this fixture, so u_tip = 0.1 + 10/100 = 0.2. - EXPECT_NEAR(state.displacement()[0U], 0.1, 1.0e-12); - EXPECT_NEAR(state.displacement()[6U], 0.2, 2.0e-10); - EXPECT_NEAR(state.externalForce()[6U], 10.0, 1.0e-12); - EXPECT_NEAR(state.internalForce()[0U], -10.0, 1.0e-9); - EXPECT_NEAR(state.internalForce()[6U], 10.0, 1.0e-9); - EXPECT_NEAR(state.reaction()[0U], -10.0, 1.0e-9); - EXPECT_NEAR(state.residual()[6U], 0.0, 1.0e-9); - EXPECT_EQ(state.endpointResults().size(), 2U); - EXPECT_EQ(state.gaussResults().size(), 2U); - EXPECT_EQ(state.stressResults().size(), 2U); + // EA/L = 100 for this fixture, so u_tip = 0.1 + 10/100 = 0.2. + EXPECT_NEAR(state.Displacement()[0U], 0.1, 1.0e-12); + EXPECT_NEAR(state.Displacement()[6U], 0.2, 2.0e-10); + EXPECT_NEAR(state.ExternalForce()[6U], 10.0, 1.0e-12); + EXPECT_NEAR(state.InternalForce()[0U], -10.0, 1.0e-9); + EXPECT_NEAR(state.InternalForce()[6U], 10.0, 1.0e-9); + EXPECT_NEAR(state.Reaction()[0U], -10.0, 1.0e-9); + EXPECT_NEAR(state.Residual()[6U], 0.0, 1.0e-9); + EXPECT_EQ(state.EndpointResults().size(), 2U); + EXPECT_EQ(state.GaussResults().size(), 2U); + EXPECT_EQ(state.StressResults().size(), 2U); } // MITC4-FLOW-001 TEST(Mitc4ShellCli, UsesExistingLifecycleAndExactlyOneFactorization) { - TempDirectory directory{"shell-order"}; - const auto input = directory.path() / "all-constrained-shell.inp"; - const auto output = directory.path() / "results.h5"; - writeText(input, allConstrainedShellDeck()); + TempDirectory directory{"shell-order"}; + const auto input = directory.Path() / "all-constrained-shell.inp"; + const auto output = directory.Path() / "results.h5"; + WriteText(input, AllConstrainedShellDeck()); - std::vector adapterEvents; - fesa::SerialParallelFor serial; - SpyLinearSolver solver{adapterEvents}; - SpyResultsWriter writer{adapterEvents}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + std::vector adapter_events; + fesa::SerialParallelFor serial; + SpyLinearSolver solver{adapter_events}; + SpyResultsWriter writer{adapter_events}; + fesa::LinearStaticAnalysis analysis{serial, solver, writer}; - ASSERT_TRUE(analysis.run({input, output}).IsOk()); - EXPECT_EQ(solver.factorizeCalls(), 1); - EXPECT_EQ(solver.solveCalls(), 1); - EXPECT_EQ(writer.writeCalls(), 1); - EXPECT_EQ( - adapterEvents, - (std::vector{ - "solver-factorize", "solver-solve", "writer-write"})); + ASSERT_TRUE(analysis.Run({input, output}).IsOk()); + EXPECT_EQ(solver.FactorizeCalls(), 1); + EXPECT_EQ(solver.SolveCalls(), 1); + EXPECT_EQ(writer.WriteCalls(), 1); + EXPECT_EQ(adapter_events, + (std::vector{"solver-factorize", "solver-solve", + "writer-write"})); } // MITC4-FLOW-002 TEST(Mitc4ShellCli, AppliesKfcForNonzeroPrescribedDisplacement) { - TempDirectory directory{"shell-prescribed"}; - const auto input = directory.path() / "prescribed-shell.inp"; - const auto output = directory.path() / "captured-results.h5"; - writeText(input, prescribedShellDeck()); + TempDirectory directory{"shell-prescribed"}; + const auto input = directory.Path() / "prescribed-shell.inp"; + const auto output = directory.Path() / "captured-results.h5"; + WriteText(input, PrescribedShellDeck()); - fesa::SerialParallelFor serial; - RecordingMklSolver solver; - CapturingResultsWriter writer; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + fesa::SerialParallelFor serial; + RecordingMklSolver solver; + CapturingResultsWriter writer; + fesa::LinearStaticAnalysis analysis{serial, solver, writer}; - const auto status = analysis.run({input, output}); - for (const auto& diagnostic : status.Diagnostics()) { - EXPECT_TRUE(status.IsOk()) - << diagnostic.code << ": " << diagnostic.message; - } - ASSERT_TRUE(status.IsOk()); - ASSERT_EQ(solver.factorizeCalls(), 1); - ASSERT_EQ(solver.solveCalls(), 1); - ASSERT_EQ(solver.factorizedDimension(), 1U); - ASSERT_EQ(solver.rhs().size(), 1U); - EXPECT_NEAR(solver.scalarStiffness(), 440.0 / 9.0, 1.0e-12); - // Ff is exactly zero, so this nonzero RHS is solely -Kfc*dc. - EXPECT_NEAR(solver.rhs()[0U], -4.0 / 9.0, 1.0e-12); + const auto status = analysis.Run({input, output}); + for (const auto& diagnostic : status.Diagnostics()) { + EXPECT_TRUE(status.IsOk()) << diagnostic.code << ": " << diagnostic.message; + } + ASSERT_TRUE(status.IsOk()); + ASSERT_EQ(solver.FactorizeCalls(), 1); + ASSERT_EQ(solver.SolveCalls(), 1); + ASSERT_EQ(solver.FactorizedDimension(), 1U); + ASSERT_EQ(solver.Rhs().size(), 1U); + EXPECT_NEAR(solver.ScalarStiffness(), 440.0 / 9.0, 1.0e-12); + // Ff is exactly zero, so this nonzero RHS is solely -Kfc*dc. + EXPECT_NEAR(solver.Rhs()[0U], -4.0 / 9.0, 1.0e-12); - EXPECT_EQ(writer.outputPath(), output); - EXPECT_EQ(writer.nodeCount(), 4U); - EXPECT_EQ(writer.shellElementCount(), 1U); - const auto& state = writer.state(); - ASSERT_EQ(state.displacement().Size(), 24U); - EXPECT_NEAR(state.displacement()[6U], 0.1, 1.0e-12); - EXPECT_NEAR(state.displacement()[12U], -1.0 / 110.0, 1.0e-12); - EXPECT_NEAR(state.verificationMetrics()[0U], 0.0, 1.0e-10); - EXPECT_EQ(state.shellResults().size(), 4U); - EXPECT_GT(state.physicalStrainEnergy(), 0.0); + EXPECT_EQ(writer.OutputPath(), output); + EXPECT_EQ(writer.NodeCount(), 4U); + EXPECT_EQ(writer.ShellElementCount(), 1U); + const auto& state = writer.State(); + ASSERT_EQ(state.Displacement().Size(), 24U); + EXPECT_NEAR(state.Displacement()[6U], 0.1, 1.0e-12); + EXPECT_NEAR(state.Displacement()[12U], -1.0 / 110.0, 1.0e-12); + EXPECT_NEAR(state.VerificationMetrics()[0U], 0.0, 1.0e-10); + EXPECT_EQ(state.ShellResults().size(), 4U); + EXPECT_GT(state.PhysicalStrainEnergy(), 0.0); } // MITC4-FLOW-003 TEST(Mitc4ShellCli, RejectsSingularAndAcceptsZeroByZeroFreeSystem) { - TempDirectory directory{"shell-singular-all"}; - const auto singularInput = directory.path() / "singular-shell.inp"; - const auto constrainedInput = directory.path() / "constrained-shell.inp"; - writeText(singularInput, shellDeck("")); - writeText(constrainedInput, allConstrainedShellDeck()); + TempDirectory directory{"shell-singular-all"}; + const auto singular_input = directory.Path() / "singular-shell.inp"; + const auto constrained_input = directory.Path() / "constrained-shell.inp"; + WriteText(singular_input, ShellDeck("")); + WriteText(constrained_input, AllConstrainedShellDeck()); - fesa::SerialParallelFor serial; - fesa::MklPardisoSolver singularSolver; - std::vector singularEvents; - SpyResultsWriter singularWriter{singularEvents}; - fesa::LinearStaticAnalysis singularAnalysis{ - serial, singularSolver, singularWriter}; - const auto singular = singularAnalysis.run( - {singularInput, directory.path() / "singular.h5"}); - ASSERT_FALSE(singular.IsOk()); - EXPECT_EQ(singular.Category(), fesa::FailureCategory::kSolver); - EXPECT_EQ(singularWriter.writeCalls(), 0); + fesa::SerialParallelFor serial; + fesa::MklPardisoSolver singular_solver; + std::vector singular_events; + SpyResultsWriter singular_writer{singular_events}; + fesa::LinearStaticAnalysis singular_analysis{serial, singular_solver, + singular_writer}; + const auto singular = + singular_analysis.Run({singular_input, directory.Path() / "singular.h5"}); + ASSERT_FALSE(singular.IsOk()); + EXPECT_EQ(singular.Category(), fesa::FailureCategory::kSolver); + EXPECT_EQ(singular_writer.WriteCalls(), 0); - RecordingMklSolver constrainedSolver; - CapturingResultsWriter constrainedWriter; - fesa::LinearStaticAnalysis constrainedAnalysis{ - serial, constrainedSolver, constrainedWriter}; - const auto constrained = constrainedAnalysis.run( - {constrainedInput, directory.path() / "constrained.h5"}); - ASSERT_TRUE(constrained.IsOk()); - EXPECT_EQ(constrainedSolver.factorizeCalls(), 1); - EXPECT_EQ(constrainedSolver.factorizedDimension(), 0U); - EXPECT_EQ(constrainedSolver.solveCalls(), 1); - EXPECT_TRUE(constrainedSolver.rhs().empty()); - EXPECT_EQ(constrainedWriter.state().shellResults().size(), 4U); + RecordingMklSolver constrained_solver; + CapturingResultsWriter constrained_writer; + fesa::LinearStaticAnalysis constrained_analysis{serial, constrained_solver, + constrained_writer}; + const auto constrained = constrained_analysis.Run( + {constrained_input, directory.Path() / "constrained.h5"}); + ASSERT_TRUE(constrained.IsOk()); + EXPECT_EQ(constrained_solver.FactorizeCalls(), 1); + EXPECT_EQ(constrained_solver.FactorizedDimension(), 0U); + EXPECT_EQ(constrained_solver.SolveCalls(), 1); + EXPECT_TRUE(constrained_solver.Rhs().empty()); + EXPECT_EQ(constrained_writer.State().ShellResults().size(), 4U); } // MITC4-FLOW-004 TEST(Mitc4ShellCli, DoesNotWriteAnInvalidRecoveryCandidate) { - TempDirectory directory{"shell-invalid-candidate"}; - const auto input = directory.path() / "invalid-recovery-shell.inp"; - writeText(input, prescribedShellDeck()); + TempDirectory directory{"shell-invalid-candidate"}; + const auto input = directory.Path() / "invalid-recovery-shell.inp"; + WriteText(input, PrescribedShellDeck()); - fesa::SerialParallelFor serial; - NonfiniteLinearSolver solver; - std::vector adapterEvents; - SpyResultsWriter writer{adapterEvents}; - fesa::LinearStaticAnalysis analysis{serial, solver, writer}; - const auto status = analysis.run( - {input, directory.path() / "must-not-exist.h5"}); + fesa::SerialParallelFor serial; + NonfiniteLinearSolver solver; + std::vector adapter_events; + SpyResultsWriter writer{adapter_events}; + fesa::LinearStaticAnalysis analysis{serial, solver, writer}; + const auto status = + analysis.Run({input, directory.Path() / "must-not-exist.h5"}); - ASSERT_FALSE(status.IsOk()); - EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); - ASSERT_FALSE(status.Diagnostics().empty()); - EXPECT_EQ(status.Diagnostics().front().code, "nonfinite-recovery-value"); - EXPECT_EQ(writer.writeCalls(), 0); - EXPECT_TRUE(adapterEvents.empty()); + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); + ASSERT_FALSE(status.Diagnostics().empty()); + EXPECT_EQ(status.Diagnostics().front().code, "nonfinite-recovery-value"); + EXPECT_EQ(writer.WriteCalls(), 0); + EXPECT_TRUE(adapter_events.empty()); } diff --git a/tests/reference/reference_comparison.cpp b/tests/reference/reference_comparison.cpp index e37e444..22bba12 100644 --- a/tests/reference/reference_comparison.cpp +++ b/tests/reference/reference_comparison.cpp @@ -1,11 +1,11 @@ #include "reference_comparison.hpp" -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/assembly/load_assembler.hpp" -#include "fesa/fem/dof_manager.hpp" +#include "fesa/analysis/analysis_model.h" +#include "fesa/assembly/load_assembler.h" +#include "fesa/fem/dof_manager.h" #include "fesa/io/abaqus/domain_mapper.hpp" #include "fesa/io/abaqus/input_reader.hpp" -#include "fesa/results/result_recovery.hpp" +#include "fesa/results/result_recovery.h" #include @@ -962,7 +962,7 @@ std::vector normalizeStations( const Domain& domain, const HdfProjection& hdf, const ReferenceTable& sectionTable) { - auto modelResult = AnalysisModel::create(domain); + auto modelResult = AnalysisModel::Create(domain); if (!modelResult.HasValue()) { fail("schema-mismatch", "The approved input cannot create an analysis view."); } @@ -991,7 +991,7 @@ std::vector normalizeStations( values}); } } - auto normalized = ResultRecovery::normalizeSectionResultantsToNodeStations( + auto normalized = ResultRecovery::NormalizeSectionResultantsToNodeStations( model, endpoints, tolerances); if (!normalized.HasValue()) { const auto& diagnostics = normalized.GetStatus().Diagnostics(); @@ -1079,7 +1079,7 @@ void appendSectionRows( for (std::size_t component = 0U; component < components.size(); ++component) { auto fesa = canonicalRow( hdf.nodes[node], ComparisonQuantity::sectionResultant, - components[component], station.sectionResultant[component], + components[component], station.section_resultant[component], units[component], "beam-local", kSectionPath); auto abaqus = canonicalRow( hdf.nodes[node], ComparisonQuantity::sectionResultant, @@ -1164,17 +1164,17 @@ void evaluateGroup( PhysicsEvidence makePhysicsEvidence( const Domain& domain, const HdfProjection& hdf) { - auto modelResult = AnalysisModel::create(domain); + auto modelResult = AnalysisModel::Create(domain); if (!modelResult.HasValue()) { fail("schema-mismatch", "The approved input cannot create physics evidence."); } const AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = DofManager::create(model); + auto dofsResult = DofManager::Create(model); if (!dofsResult.HasValue()) { fail("schema-mismatch", "The approved input cannot create a DOF map."); } const DofManager dofs = std::move(dofsResult.Value()); - auto loadResult = LoadAssembler::assembleFullNodalLoad(model, dofs); + auto loadResult = LoadAssembler::AssembleFullNodalLoad(model, dofs); if (!loadResult.HasValue()) { fail("schema-mismatch", "The approved input load cannot be assembled."); } @@ -1185,7 +1185,7 @@ PhysicsEvidence makePhysicsEvidence( PhysicsEvidence evidence{}; long double residualSquared = 0.0L; - for (const std::size_t freeDof : dofs.freeDofs()) { + for (const std::size_t freeDof : dofs.FreeDofs()) { const long double value = static_cast(hdf.reaction[freeDof]); residualSquared += value * value; diff --git a/tests/reference/reference_comparison_test.cpp b/tests/reference/reference_comparison_test.cpp index 252a335..9ec0267 100644 --- a/tests/reference/reference_comparison_test.cpp +++ b/tests/reference/reference_comparison_test.cpp @@ -1,8 +1,8 @@ #include "reference_comparison.hpp" -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/fem/dof_manager.hpp" +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/fem/dof_manager.h" #include "fesa/io/abaqus/input_reader.hpp" #include "fesa/io/hdf5/hdf5_results_writer.hpp" #include "fesa/model/domain.h" @@ -223,42 +223,42 @@ void writeResultsFixture( throw std::runtime_error{"Reference fixture Domain construction failed."}; } fesa::Domain domain = std::move(domainResult.Value()); - auto modelResult = fesa::AnalysisModel::create(domain); + auto modelResult = fesa::AnalysisModel::Create(domain); if (!modelResult.HasValue()) { throw std::runtime_error{"Reference fixture AnalysisModel construction failed."}; } fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::create(model); + auto dofsResult = fesa::DofManager::Create(model); if (!dofsResult.HasValue()) { throw std::runtime_error{"Reference fixture DofManager construction failed."}; } fesa::DofManager dofs = std::move(dofsResult.Value()); fesa::AnalysisState state = - fesa::AnalysisState::create(dofs, {"Step-1", 0U}); + fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); for (std::size_t node = 0U; node < kNodeCount; ++node) { for (std::size_t component = 0U; component < 6U; ++component) { const std::size_t index = node * 6U + component; - state.displacement()[index] = values.displacement[node][component]; - state.reaction()[index] = values.reaction[node][component]; - state.residual()[index] = values.reaction[node][component]; + state.Displacement()[index] = values.displacement[node][component]; + state.Reaction()[index] = values.reaction[node][component]; + state.Residual()[index] = values.reaction[node][component]; } } for (std::size_t element = 0U; element < kElementCount; ++element) { for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { const std::size_t node = element + endpoint; - state.endpointResults().push_back({ + state.EndpointResults().push_back({ static_cast(element), static_cast(endpoint), domain.Nodes()[node].source_id, {}, values.sectionResultants[element][endpoint]}); } - state.gaussResults().push_back( + state.GaussResults().push_back( {static_cast(element), 1, {}, {}}); - state.gaussResults().push_back( + state.GaussResults().push_back( {static_cast(element), 2, {}, {}}); - state.stressResults().push_back({ + state.StressResults().push_back({ static_cast(element), 1, 0U, @@ -266,7 +266,7 @@ void writeResultsFixture( 0.0, 0.0, "fesa-default"}); - state.stressResults().push_back({ + state.StressResults().push_back({ static_cast(element), 2, 0U, @@ -277,7 +277,7 @@ void writeResultsFixture( } fesa::Hdf5ResultsWriter writer; - const fesa::Status status = writer.write(output, domain, state, {}); + const fesa::Status status = writer.Write(output, domain, state, {}); if (!status.IsOk()) { throw std::runtime_error{"Reference fixture HDF5 write failed."}; } diff --git a/tests/unit/analysis/analysis_model_test.cpp b/tests/unit/analysis/analysis_model_test.cpp index 54c4d52..7fb8cdd 100644 --- a/tests/unit/analysis/analysis_model_test.cpp +++ b/tests/unit/analysis/analysis_model_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/analysis/analysis_model.hpp" +#include "fesa/analysis/analysis_model.h" #include @@ -9,146 +9,154 @@ namespace { -fesa::ModelDefinition makeDefinition() { - const std::filesystem::path source{"models/analysis-model.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 11U}}, - {{"Beam-1", 3, "3"}, {2.0, 0.0, 0.0}, {source, 12U}}, - {{"Beam-1", 4, "4"}, {3.0, 0.0, 0.0}, {source, 13U}}, - {{"Beam-1", 5, "5"}, {4.0, 0.0, 0.0}, {source, 14U}}}; - definition.materials = { - {"Material-0", 100.0, 0.20, {source, 20U}}, - {"Material-1", 200.0, 0.25, {source, 21U}}, - {"Material-2", 300.0, 0.30, {source, 22U}}, - {"Unused-Material", 400.0, 0.35, {source, 23U}}}; - definition.sections = { - {"Section-0", 1.0, 1.0, 0.0, 1.0, 1.0, - {0.0, 1.0, 0.0}, {}, {source, 30U}}, - {"Section-1", 2.0, 2.0, 0.0, 2.0, 2.0, - {0.0, 1.0, 0.0}, {}, {source, 31U}}, - {"Section-2", 3.0, 3.0, 0.0, 3.0, 3.0, - {0.0, 1.0, 0.0}, {}, {source, 32U}}, - {"Unused-Section", 4.0, 4.0, 0.0, 4.0, 4.0, - {0.0, 1.0, 0.0}, {}, {source, 33U}}}; - definition.elements = { - {{"Beam-1", 1, "1"}, {0U, 1U}, 2U, 2U, {source, 40U}}, - {{"Beam-1", 2, "2"}, {1U, 2U}, 0U, 1U, {source, 41U}}, - {{"Beam-1", 3, "3"}, {2U, 3U}, 2U, 2U, {source, 42U}}, - {{"Beam-1", 4, "4"}, {3U, 4U}, 1U, 0U, {source, 43U}}}; - definition.steps = {{ - "Step-1", - {{"Root", 1, 3, 0.0, {source, 51U}}, - {"Root", 4, 6, 0.0, {source, 52U}}}, - {{"Tip", 1, 10.0, {source, 53U}}, - {"Tip", 2, -20.0, {source, 54U}}, - {"Tip", 6, 30.0, {source, 55U}}}, - 0.1, - 1.0, - 0.01, - 1.0, - {source, 50U}}}; - return definition; +fesa::ModelDefinition MakeDefinition() { + const std::filesystem::path source{"models/analysis-model.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = {{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 11U}}, + {{"Beam-1", 3, "3"}, {2.0, 0.0, 0.0}, {source, 12U}}, + {{"Beam-1", 4, "4"}, {3.0, 0.0, 0.0}, {source, 13U}}, + {{"Beam-1", 5, "5"}, {4.0, 0.0, 0.0}, {source, 14U}}}; + definition.materials = {{"Material-0", 100.0, 0.20, {source, 20U}}, + {"Material-1", 200.0, 0.25, {source, 21U}}, + {"Material-2", 300.0, 0.30, {source, 22U}}, + {"Unused-Material", 400.0, 0.35, {source, 23U}}}; + definition.sections = {{"Section-0", + 1.0, + 1.0, + 0.0, + 1.0, + 1.0, + {0.0, 1.0, 0.0}, + {}, + {source, 30U}}, + {"Section-1", + 2.0, + 2.0, + 0.0, + 2.0, + 2.0, + {0.0, 1.0, 0.0}, + {}, + {source, 31U}}, + {"Section-2", + 3.0, + 3.0, + 0.0, + 3.0, + 3.0, + {0.0, 1.0, 0.0}, + {}, + {source, 32U}}, + {"Unused-Section", + 4.0, + 4.0, + 0.0, + 4.0, + 4.0, + {0.0, 1.0, 0.0}, + {}, + {source, 33U}}}; + definition.elements = {{{"Beam-1", 1, "1"}, {0U, 1U}, 2U, 2U, {source, 40U}}, + {{"Beam-1", 2, "2"}, {1U, 2U}, 0U, 1U, {source, 41U}}, + {{"Beam-1", 3, "3"}, {2U, 3U}, 2U, 2U, {source, 42U}}, + {{"Beam-1", 4, "4"}, {3U, 4U}, 1U, 0U, {source, 43U}}}; + definition.steps = { + {"Step-1", + {{"Root", 1, 3, 0.0, {source, 51U}}, {"Root", 4, 6, 0.0, {source, 52U}}}, + {{"Tip", 1, 10.0, {source, 53U}}, + {"Tip", 2, -20.0, {source, 54U}}, + {"Tip", 6, 30.0, {source, 55U}}}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 50U}}}; + return definition; } -} // namespace +} // namespace TEST(AnalysisModel, ClassifiesActiveEntitiesInStableOrder) { - auto domainResult = fesa::Domain::Create(makeDefinition()); - ASSERT_TRUE(domainResult.HasValue()); + auto domain_result = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain_result.HasValue()); - auto modelResult = fesa::AnalysisModel::create(domainResult.Value()); - ASSERT_TRUE(modelResult.HasValue()); - const auto& model = modelResult.Value(); + auto model_result = fesa::AnalysisModel::Create(domain_result.Value()); + ASSERT_TRUE(model_result.HasValue()); + const auto& model = model_result.Value(); - EXPECT_EQ( - model.activeElements(), - (std::vector{0U, 1U, 2U, 3U})); - EXPECT_EQ( - model.activeMaterials(), - (std::vector{0U, 1U, 2U})); - EXPECT_EQ( - model.activeSections(), - (std::vector{0U, 1U, 2U})); - EXPECT_EQ( - model.activeBoundaryConditions(), - (std::vector{0U, 1U})); - EXPECT_EQ( - model.activeLoads(), - (std::vector{0U, 1U, 2U})); + EXPECT_EQ(model.ActiveElements(), + (std::vector{0U, 1U, 2U, 3U})); + EXPECT_EQ(model.ActiveMaterials(), + (std::vector{0U, 1U, 2U})); + EXPECT_EQ(model.ActiveSections(), + (std::vector{0U, 1U, 2U})); + EXPECT_EQ(model.ActiveBoundaryConditions(), + (std::vector{0U, 1U})); + EXPECT_EQ(model.ActiveLoads(), (std::vector{0U, 1U, 2U})); } TEST(AnalysisModel, ReferencesWithoutCopyingOrMutatingDomain) { - auto domainResult = fesa::Domain::Create(makeDefinition()); - ASSERT_TRUE(domainResult.HasValue()); - const fesa::Domain& domain = domainResult.Value(); - const auto* const elementAddress = domain.Elements().data(); - const auto* const materialAddress = domain.Materials().data(); - const auto* const sectionAddress = domain.Sections().data(); - const std::string stepName = domain.Steps()[0].name; - const double firstLoadMagnitude = domain.Steps()[0].loads[0].magnitude; + auto domain_result = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain_result.HasValue()); + const fesa::Domain& domain = domain_result.Value(); + const auto* const element_address = domain.Elements().data(); + const auto* const material_address = domain.Materials().data(); + const auto* const section_address = domain.Sections().data(); + const std::string step_name = domain.Steps()[0].name; + const double first_load_magnitude = domain.Steps()[0].loads[0].magnitude; - auto modelResult = fesa::AnalysisModel::create(domain); - ASSERT_TRUE(modelResult.HasValue()); - const auto& model = modelResult.Value(); + auto model_result = fesa::AnalysisModel::Create(domain); + ASSERT_TRUE(model_result.HasValue()); + const auto& model = model_result.Value(); - EXPECT_EQ(&model.domain(), &domain); - EXPECT_EQ(&model.step(), &domain.Steps()[0]); - EXPECT_EQ(model.domain().Elements().data(), elementAddress); - EXPECT_EQ(model.domain().Materials().data(), materialAddress); - EXPECT_EQ(model.domain().Sections().data(), sectionAddress); - EXPECT_EQ( - &model.domain().Elements()[model.activeElements()[1]], - &domain.Elements()[1]); - EXPECT_EQ( - &model.domain().Materials()[model.activeMaterials()[2]], - &domain.Materials()[2]); - EXPECT_EQ( - &model.domain().Sections()[model.activeSections()[1]], - &domain.Sections()[1]); - EXPECT_EQ(domain.Steps()[0].name, stepName); - EXPECT_DOUBLE_EQ(domain.Steps()[0].loads[0].magnitude, firstLoadMagnitude); + EXPECT_EQ(&model.GetDomain(), &domain); + EXPECT_EQ(&model.Step(), &domain.Steps()[0]); + EXPECT_EQ(model.GetDomain().Elements().data(), element_address); + EXPECT_EQ(model.GetDomain().Materials().data(), material_address); + EXPECT_EQ(model.GetDomain().Sections().data(), section_address); + EXPECT_EQ(&model.GetDomain().Elements()[model.ActiveElements()[1]], + &domain.Elements()[1]); + EXPECT_EQ(&model.GetDomain().Materials()[model.ActiveMaterials()[2]], + &domain.Materials()[2]); + EXPECT_EQ(&model.GetDomain().Sections()[model.ActiveSections()[1]], + &domain.Sections()[1]); + EXPECT_EQ(domain.Steps()[0].name, step_name); + EXPECT_DOUBLE_EQ(domain.Steps()[0].loads[0].magnitude, first_load_magnitude); } TEST(AnalysisModel, RejectsMissingOrMultipleStep) { - auto missingDefinition = makeDefinition(); - missingDefinition.steps.clear(); - auto missingDomain = fesa::Domain::Create(std::move(missingDefinition)); - ASSERT_TRUE(missingDomain.HasValue()); + auto missing_definition = MakeDefinition(); + missing_definition.steps.clear(); + auto missing_domain = fesa::Domain::Create(std::move(missing_definition)); + ASSERT_TRUE(missing_domain.HasValue()); - auto missing = fesa::AnalysisModel::create(missingDomain.Value()); - ASSERT_FALSE(missing.HasValue()); - EXPECT_EQ( - missing.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_EQ(missing.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ( - missing.GetStatus().Diagnostics()[0].code, - "invalid-model-cardinality"); - EXPECT_EQ(missing.GetStatus().Diagnostics()[0].keyword, "STEP"); - EXPECT_EQ(missing.GetStatus().Diagnostics()[0].entity_identity, "0"); + auto missing = fesa::AnalysisModel::Create(missing_domain.Value()); + ASSERT_FALSE(missing.HasValue()); + EXPECT_EQ(missing.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_EQ(missing.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(missing.GetStatus().Diagnostics()[0].code, + "invalid-model-cardinality"); + EXPECT_EQ(missing.GetStatus().Diagnostics()[0].keyword, "STEP"); + EXPECT_EQ(missing.GetStatus().Diagnostics()[0].entity_identity, "0"); - auto multipleDefinition = makeDefinition(); - auto secondStep = multipleDefinition.steps.front(); - secondStep.name = "Step-2"; - secondStep.location.line = 60U; - multipleDefinition.steps.push_back(std::move(secondStep)); - auto multipleDomain = fesa::Domain::Create(std::move(multipleDefinition)); - ASSERT_TRUE(multipleDomain.HasValue()); + auto multiple_definition = MakeDefinition(); + auto second_step = multiple_definition.steps.front(); + second_step.name = "Step-2"; + second_step.location.line = 60U; + multiple_definition.steps.push_back(std::move(second_step)); + auto multiple_domain = fesa::Domain::Create(std::move(multiple_definition)); + ASSERT_TRUE(multiple_domain.HasValue()); - auto multiple = fesa::AnalysisModel::create(multipleDomain.Value()); - ASSERT_FALSE(multiple.HasValue()); - EXPECT_EQ( - multiple.GetStatus().Category(), - fesa::FailureCategory::kInput); - ASSERT_EQ(multiple.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ( - multiple.GetStatus().Diagnostics()[0].code, - "unsupported-multiple-step"); - EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].keyword, "STEP"); - EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].entity_identity, "Step-2"); - EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].location.line, 60U); + auto multiple = fesa::AnalysisModel::Create(multiple_domain.Value()); + ASSERT_FALSE(multiple.HasValue()); + EXPECT_EQ(multiple.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_EQ(multiple.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].code, + "unsupported-multiple-step"); + EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].keyword, "STEP"); + EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].entity_identity, "Step-2"); + EXPECT_EQ(multiple.GetStatus().Diagnostics()[0].location.line, 60U); } diff --git a/tests/unit/analysis/analysis_state_test.cpp b/tests/unit/analysis/analysis_state_test.cpp index b338179..ea2247b 100644 --- a/tests/unit/analysis/analysis_state_test.cpp +++ b/tests/unit/analysis/analysis_state_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/analysis/analysis_state.hpp" +#include "fesa/analysis/analysis_state.h" #include @@ -9,165 +9,162 @@ namespace { -template +template struct HasVelocity : std::false_type {}; -template -struct HasVelocity().velocity())>> +template +struct HasVelocity().Velocity())>> : std::true_type {}; -template +template struct HasAcceleration : std::false_type {}; -template -struct HasAcceleration< - T, std::void_t().acceleration())>> +template +struct HasAcceleration().Acceleration())>> : std::true_type {}; -template +template struct HasTemperature : std::false_type {}; -template -struct HasTemperature().temperature())>> +template +struct HasTemperature().Temperature())>> : std::true_type {}; -template +template struct HasIterationHistory : std::false_type {}; -template +template struct HasIterationHistory< - T, std::void_t().iterationHistory())>> + T, std::void_t().IterationHistory())>> : std::true_type {}; -template +template struct HasNonlinearState : std::false_type {}; -template +template struct HasNonlinearState< - T, std::void_t().nonlinearState())>> + T, std::void_t().NonlinearState())>> : std::true_type {}; -fesa::DofManager makeDofs() { - fesa::ModelDefinition definition{}; - definition.source_path = "models/analysis-state.inp"; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {definition.source_path, 10U}}, - {{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {definition.source_path, 11U}}}; - definition.steps = {{ - "Step-1", - {{"1", 1, 6, 0.0, {definition.source_path, 20U}}}, - {}, - 0.1, - 1.0, - 0.01, - 1.0, - {definition.source_path, 19U}}}; +fesa::DofManager MakeDofs() { + fesa::ModelDefinition definition{}; + definition.source_path = "models/analysis-state.inp"; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = { + {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {definition.source_path, 10U}}, + {{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {definition.source_path, 11U}}}; + definition.steps = {{"Step-1", + {{"1", 1, 6, 0.0, {definition.source_path, 20U}}}, + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {definition.source_path, 19U}}}; - auto domain = fesa::Domain::Create(std::move(definition)); - EXPECT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - EXPECT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - EXPECT_TRUE(dofs.HasValue()); - return std::move(dofs.Value()); + auto domain = fesa::Domain::Create(std::move(definition)); + EXPECT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + EXPECT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + EXPECT_TRUE(dofs.HasValue()); + return std::move(dofs.Value()); } -void expectAllZero(const fesa::Vector& vector) { - for (std::size_t index = 0U; index < vector.Size(); ++index) { - EXPECT_DOUBLE_EQ(vector[index], 0.0); - } +void ExpectAllZero(const fesa::Vector& vector) { + for (std::size_t index = 0U; index < vector.Size(); ++index) { + EXPECT_DOUBLE_EQ(vector[index], 0.0); + } } -} // namespace +} // namespace TEST(AnalysisState, AllocatesOnlyV0FullVectors) { - const auto dofs = makeDofs(); - ASSERT_EQ(dofs.fullDofCount(), 12U); - ASSERT_EQ(dofs.constrainedDofCount(), 6U); + const auto dofs = MakeDofs(); + ASSERT_EQ(dofs.FullDofCount(), 12U); + ASSERT_EQ(dofs.ConstrainedDofCount(), 6U); - auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); - const fesa::AnalysisState& constState = state; - const fesa::Vector* const vectors[] = { - &constState.displacement(), - &constState.externalForce(), - &constState.internalForce(), - &constState.residual(), - &constState.reaction()}; + auto state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); + const fesa::AnalysisState& const_state = state; + const fesa::Vector* const vectors[] = { + &const_state.Displacement(), &const_state.ExternalForce(), + &const_state.InternalForce(), &const_state.Residual(), + &const_state.Reaction()}; - for (const auto* vector : vectors) { - EXPECT_EQ(vector->Size(), dofs.fullDofCount()); - expectAllZero(*vector); - } - for (std::size_t left = 0U; left < std::size(vectors); ++left) { - for (std::size_t right = left + 1U; right < std::size(vectors); ++right) { - EXPECT_NE(vectors[left]->Data(), vectors[right]->Data()); - } + for (const auto* vector : vectors) { + EXPECT_EQ(vector->Size(), dofs.FullDofCount()); + ExpectAllZero(*vector); + } + for (std::size_t left = 0U; left < std::size(vectors); ++left) { + for (std::size_t right = left + 1U; right < std::size(vectors); ++right) { + EXPECT_NE(vectors[left]->Data(), vectors[right]->Data()); } + } - state.displacement()[0] = 4.0; - state.reaction()[11] = -9.0; - EXPECT_DOUBLE_EQ(state.displacement()[0], 4.0); - EXPECT_DOUBLE_EQ(state.reaction()[11], -9.0); - EXPECT_DOUBLE_EQ(state.externalForce()[0], 0.0); - EXPECT_DOUBLE_EQ(state.internalForce()[0], 0.0); - EXPECT_DOUBLE_EQ(state.residual()[0], 0.0); + state.Displacement()[0] = 4.0; + state.Reaction()[11] = -9.0; + EXPECT_DOUBLE_EQ(state.Displacement()[0], 4.0); + EXPECT_DOUBLE_EQ(state.Reaction()[11], -9.0); + EXPECT_DOUBLE_EQ(state.ExternalForce()[0], 0.0); + EXPECT_DOUBLE_EQ(state.InternalForce()[0], 0.0); + EXPECT_DOUBLE_EQ(state.Residual()[0], 0.0); - EXPECT_FALSE(HasVelocity::value); - EXPECT_FALSE(HasAcceleration::value); - EXPECT_FALSE(HasTemperature::value); - EXPECT_FALSE(HasIterationHistory::value); - EXPECT_FALSE(HasNonlinearState::value); + EXPECT_FALSE(HasVelocity::value); + EXPECT_FALSE(HasAcceleration::value); + EXPECT_FALSE(HasTemperature::value); + EXPECT_FALSE(HasIterationHistory::value); + EXPECT_FALSE(HasNonlinearState::value); } TEST(AnalysisState, CopiesOrMovesWithoutAliasing) { - static_assert(std::is_copy_constructible_v); - static_assert(std::is_copy_assignable_v); - static_assert(std::is_move_constructible_v); - static_assert(std::is_move_assignable_v); + static_assert(std::is_copy_constructible_v); + static_assert(std::is_copy_assignable_v); + static_assert(std::is_move_constructible_v); + static_assert(std::is_move_assignable_v); - const auto dofs = makeDofs(); - auto original = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); - original.displacement()[0] = 3.0; - original.reaction()[11] = -2.0; - original.endpointResults().push_back({ - 0U, - -1, - {"Beam-1", 1, "1"}, - {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {2.0, 0.0, 0.0, 0.0}}); - original.gaussResults().push_back( - {0U, 1, {0.1, 0.0, 0.0, 0.0}, {10.0, 0.0, 0.0, 0.0}}); - original.stressResults().push_back( - {0U, 1, 0U, 0.0, 0.0, 10.0, "fesa-default"}); + const auto dofs = MakeDofs(); + auto original = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); + original.Displacement()[0] = 3.0; + original.Reaction()[11] = -2.0; + original.EndpointResults().push_back({0U, + -1, + {"Beam-1", 1, "1"}, + {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {2.0, 0.0, 0.0, 0.0}}); + original.GaussResults().push_back( + {0U, 1, {0.1, 0.0, 0.0, 0.0}, {10.0, 0.0, 0.0, 0.0}}); + original.StressResults().push_back( + {0U, 1, 0U, 0.0, 0.0, 10.0, "fesa-default"}); - auto copied = original; - EXPECT_NE(copied.displacement().Data(), original.displacement().Data()); - EXPECT_NE(copied.reaction().Data(), original.reaction().Data()); - EXPECT_NE(copied.endpointResults().data(), original.endpointResults().data()); - EXPECT_NE(copied.gaussResults().data(), original.gaussResults().data()); - EXPECT_NE(copied.stressResults().data(), original.stressResults().data()); - copied.displacement()[0] = 30.0; - copied.endpointResults()[0].endAction[0] = 20.0; - EXPECT_DOUBLE_EQ(original.displacement()[0], 3.0); - EXPECT_DOUBLE_EQ(original.endpointResults()[0].endAction[0], 1.0); + auto copied = original; + EXPECT_NE(copied.Displacement().Data(), original.Displacement().Data()); + EXPECT_NE(copied.Reaction().Data(), original.Reaction().Data()); + EXPECT_NE(copied.EndpointResults().data(), original.EndpointResults().data()); + EXPECT_NE(copied.GaussResults().data(), original.GaussResults().data()); + EXPECT_NE(copied.StressResults().data(), original.StressResults().data()); + copied.Displacement()[0] = 30.0; + copied.EndpointResults()[0].end_action[0] = 20.0; + EXPECT_DOUBLE_EQ(original.Displacement()[0], 3.0); + EXPECT_DOUBLE_EQ(original.EndpointResults()[0].end_action[0], 1.0); - auto moved = std::move(copied); - EXPECT_EQ(moved.identity().stepName, "Step-1"); - EXPECT_DOUBLE_EQ(moved.displacement()[0], 30.0); - EXPECT_DOUBLE_EQ(moved.endpointResults()[0].endAction[0], 20.0); - EXPECT_NE(moved.displacement().Data(), original.displacement().Data()); - EXPECT_NE(moved.endpointResults().data(), original.endpointResults().data()); + auto moved = std::move(copied); + EXPECT_EQ(moved.Identity().step_name, "Step-1"); + EXPECT_DOUBLE_EQ(moved.Displacement()[0], 30.0); + EXPECT_DOUBLE_EQ(moved.EndpointResults()[0].end_action[0], 20.0); + EXPECT_NE(moved.Displacement().Data(), original.Displacement().Data()); + EXPECT_NE(moved.EndpointResults().data(), original.EndpointResults().data()); - auto copyAssigned = fesa::AnalysisState::create(dofs, {"Other", 3U}); - copyAssigned = original; - copyAssigned.reaction()[11] = -20.0; - EXPECT_DOUBLE_EQ(original.reaction()[11], -2.0); - EXPECT_EQ(copyAssigned.identity().stepName, "Step-1"); + auto copy_assigned = fesa::AnalysisState::Create(dofs, {"Other", 3U}); + copy_assigned = original; + copy_assigned.Reaction()[11] = -20.0; + EXPECT_DOUBLE_EQ(original.Reaction()[11], -2.0); + EXPECT_EQ(copy_assigned.Identity().step_name, "Step-1"); - auto moveAssigned = fesa::AnalysisState::create(dofs, {"Other", 4U}); - moveAssigned = std::move(copyAssigned); - EXPECT_EQ(moveAssigned.identity().stepName, "Step-1"); - EXPECT_DOUBLE_EQ(moveAssigned.reaction()[11], -20.0); - EXPECT_NE(moveAssigned.reaction().Data(), original.reaction().Data()); + auto move_assigned = fesa::AnalysisState::Create(dofs, {"Other", 4U}); + move_assigned = std::move(copy_assigned); + EXPECT_EQ(move_assigned.Identity().step_name, "Step-1"); + EXPECT_DOUBLE_EQ(move_assigned.Reaction()[11], -20.0); + EXPECT_NE(move_assigned.Reaction().Data(), original.Reaction().Data()); } diff --git a/tests/unit/assembly/load_assembler_test.cpp b/tests/unit/assembly/load_assembler_test.cpp index 0e86edd..64b4093 100644 --- a/tests/unit/assembly/load_assembler_test.cpp +++ b/tests/unit/assembly/load_assembler_test.cpp @@ -1,8 +1,4 @@ -#include "fesa/assembly/load_assembler.hpp" - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/model/domain.h" +#include "fesa/assembly/load_assembler.h" #include @@ -18,489 +14,410 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + namespace { struct LoadFixture { - std::unique_ptr domain; - std::unique_ptr model; - std::unique_ptr dofs; + std::unique_ptr domain; + std::unique_ptr model; + std::unique_ptr dofs; }; -LoadFixture makeFixture( - const std::size_t nodeCount, - std::vector nodeSets, - std::vector boundaries, - std::vector loads) { - const std::filesystem::path source{"models/load-assembly.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:abcdef0123456789"; - for (std::size_t index = 0U; index < nodeCount; ++index) { - const auto label = static_cast((index + 1U) * 10U); - definition.nodes.push_back({ - {"Beam-1", label, std::to_string(label)}, - {static_cast(index), 0.0, 0.0}, - {source, index + 2U}}); - } - definition.node_sets = std::move(nodeSets); - definition.steps = {{ - "Step-1", - std::move(boundaries), - std::move(loads), - 0.1, - 1.0, - 0.01, - 1.0, - {source, 20U}}}; +LoadFixture MakeFixture(const std::size_t node_count, + std::vector node_sets, + std::vector boundaries, + std::vector loads) { + const std::filesystem::path source{"models/load-assembly.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:abcdef0123456789"; + for (std::size_t index = 0U; index < node_count; ++index) { + const auto label = static_cast((index + 1U) * 10U); + definition.nodes.push_back({{"Beam-1", label, std::to_string(label)}, + {static_cast(index), 0.0, 0.0}, + {source, index + 2U}}); + } + definition.node_sets = std::move(node_sets); + definition.steps = {{"Step-1", + std::move(boundaries), + std::move(loads), + 0.1, + 1.0, + 0.01, + 1.0, + {source, 20U}}}; - auto domainResult = fesa::Domain::Create(std::move(definition)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Load fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); + auto domain_result = fesa::Domain::Create(std::move(definition)); + if (!domain_result.HasValue()) { + throw std::runtime_error{"Load fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Load fixture AnalysisModel construction failed."}; - } - auto model = std::make_unique( - std::move(modelResult.Value())); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{"Load fixture AnalysisModel construction failed."}; + } + auto model = + std::make_unique(std::move(model_result.Value())); - auto dofResult = fesa::DofManager::create(*model); - if (!dofResult.HasValue()) { - throw std::runtime_error{"Load fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofResult.Value())); - return {std::move(domain), std::move(model), std::move(dofs)}; + auto dof_result = fesa::DofManager::Create(*model); + if (!dof_result.HasValue()) { + throw std::runtime_error{"Load fixture DofManager construction failed."}; + } + auto dofs = std::make_unique(std::move(dof_result.Value())); + return {std::move(domain), std::move(model), std::move(dofs)}; } -LoadFixture makeShellFixture( - std::vector boundaries, - std::vector loads) { - const std::filesystem::path source{"models/shell-load-assembly.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:1122334455667788"; - definition.nodes = { - {{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 2U}}, - {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 3U}}, - {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 4U}}, - {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 5U}}}; - definition.materials = { - {"Material", 1000.0, 0.25, {source, 6U}}}; - definition.shell_sections = { - {"ShellSection", 0.1, 0U, {source, 7U}}}; - definition.shell_elements = {{ - {"Shell-1", 1, "1"}, - fesa::ShellSourceElementType::kS4, - {0U, 1U, 2U, 3U}, - 0U, - 0U, - {source, 8U}}}; - 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), - 0.1, - 1.0, - 0.01, - 1.0, - {source, 20U}}}; +LoadFixture MakeShellFixture(std::vector boundaries, + std::vector loads) { + const std::filesystem::path source{"models/shell-load-assembly.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:1122334455667788"; + definition.nodes = {{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 2U}}, + {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 3U}}, + {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 4U}}, + {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 5U}}}; + definition.materials = {{"Material", 1000.0, 0.25, {source, 6U}}}; + definition.shell_sections = {{"ShellSection", 0.1, 0U, {source, 7U}}}; + definition.shell_elements = {{{"Shell-1", 1, "1"}, + fesa::ShellSourceElementType::kS4, + {0U, 1U, 2U, 3U}, + 0U, + 0U, + {source, 8U}}}; + 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), + 0.1, + 1.0, + 0.01, + 1.0, + {source, 20U}}}; - auto domainResult = fesa::Domain::Create(std::move(definition)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Shell load fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); + auto domain_result = fesa::Domain::Create(std::move(definition)); + if (!domain_result.HasValue()) { + throw std::runtime_error{"Shell load fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Shell load fixture AnalysisModel construction failed."}; - } - auto model = std::make_unique( - std::move(modelResult.Value())); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Shell load fixture AnalysisModel construction failed."}; + } + auto model = + std::make_unique(std::move(model_result.Value())); - auto dofResult = fesa::DofManager::create(*model); - if (!dofResult.HasValue()) { - throw std::runtime_error{"Shell load fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofResult.Value())); - return {std::move(domain), std::move(model), std::move(dofs)}; + auto dof_result = fesa::DofManager::Create(*model); + if (!dof_result.HasValue()) { + throw std::runtime_error{ + "Shell load fixture DofManager construction failed."}; + } + auto dofs = std::make_unique(std::move(dof_result.Value())); + return {std::move(domain), std::move(model), std::move(dofs)}; } -fesa::SparseMatrix makeDenseSparse( - const std::size_t rows, - const std::size_t columns, - const std::vector& values) { - if (values.size() != rows * columns) { - throw std::invalid_argument{"Dense sparse fixture has the wrong value count."}; - } +fesa::SparseMatrix MakeDenseSparse(const std::size_t rows, + const std::size_t columns, + const std::vector& values) { + if (values.size() != rows * columns) { + throw std::invalid_argument{ + "Dense sparse fixture has the wrong value count."}; + } - fesa::SparsePattern pattern; - std::vector contributions; - pattern.rowOffsets.reserve(rows + 1U); - pattern.rowOffsets.push_back(0U); - for (std::size_t row = 0U; row < rows; ++row) { - for (std::size_t column = 0U; column < columns; ++column) { - pattern.columnIndices.push_back(column); - contributions.push_back({ - row, - column, - values[row * columns + column], - row, - column}); - } - pattern.rowOffsets.push_back(pattern.columnIndices.size()); + fesa::SparsePattern pattern; + std::vector contributions; + pattern.row_offsets.reserve(rows + 1U); + pattern.row_offsets.push_back(0U); + for (std::size_t row = 0U; row < rows; ++row) { + for (std::size_t column = 0U; column < columns; ++column) { + pattern.column_indices.push_back(column); + contributions.push_back( + {row, column, values[row * columns + column], row, column}); } + pattern.row_offsets.push_back(pattern.column_indices.size()); + } - auto result = fesa::SparseMatrix::FromCoo( - rows, columns, std::move(contributions), pattern); - if (!result.HasValue()) { - throw std::runtime_error{"Sparse fixture construction failed."}; - } - return std::move(result.Value()); + auto result = fesa::SparseMatrix::FromCoo(rows, columns, + std::move(contributions), pattern); + if (!result.HasValue()) { + throw std::runtime_error{"Sparse fixture construction failed."}; + } + return std::move(result.Value()); } -void expectFailureCode( - const fesa::Result& result, - const std::string& code) { - ASSERT_FALSE(result.HasValue()); - EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel); - ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code, code); +void ExpectFailureCode(const fesa::Result& result, + const std::string& code) { + ASSERT_FALSE(result.HasValue()); + EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code, code); } -} // namespace +} // namespace TEST(LoadAssembly, AssemblesNodeSetAndSixComponentLoads) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto fixture = makeFixture( - 2U, - {{"Pair", std::nullopt, {0U, 1U}, {source, 10U}}}, - {{"10", 1, 1, 0.0, {source, 21U}}}, - {{"pair", 1, 1.0, {source, 30U}}, - {"10", 2, 2.0, {source, 31U}}, - {"20", 3, 3.0, {source, 32U}}, - {"10", 4, -4.0, {source, 33U}}, - {"PAIR", 5, 5.0, {source, 34U}}, - {"20", 6, 6.0, {source, 35U}}}); + const std::filesystem::path source{"models/load-assembly.inp"}; + auto fixture = + MakeFixture(2U, {{"Pair", std::nullopt, {0U, 1U}, {source, 10U}}}, + {{"10", 1, 1, 0.0, {source, 21U}}}, + {{"pair", 1, 1.0, {source, 30U}}, + {"10", 2, 2.0, {source, 31U}}, + {"20", 3, 3.0, {source, 32U}}, + {"10", 4, -4.0, {source, 33U}}, + {"PAIR", 5, 5.0, {source, 34U}}, + {"20", 6, 6.0, {source, 35U}}}); - auto result = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); - ASSERT_TRUE(result.HasValue()); - ASSERT_EQ(result.Value().Size(), 12U); - EXPECT_EQ( - std::vector(result.Value().Data(), result.Value().Data() + 12U), - (std::vector{ - 1.0, 2.0, 0.0, -4.0, 5.0, 0.0, - 1.0, 0.0, 3.0, 0.0, 5.0, 6.0})); - EXPECT_EQ(fixture.dofs->constrainedDofs(), - (std::vector{0U})); - EXPECT_DOUBLE_EQ(result.Value()[0U], 1.0); + auto result = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value().Size(), 12U); + EXPECT_EQ( + std::vector(result.Value().Data(), result.Value().Data() + 12U), + (std::vector{1.0, 2.0, 0.0, -4.0, 5.0, 0.0, 1.0, 0.0, 3.0, 0.0, + 5.0, 6.0})); + EXPECT_EQ(fixture.dofs->ConstrainedDofs(), (std::vector{0U})); + EXPECT_DOUBLE_EQ(result.Value()[0U], 1.0); } TEST(LoadAssembly, AccumulatesSignedLoadsInSourceOrder) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto firstOrder = makeFixture( - 1U, - {}, - {}, - {{"10", 1, 1.0e16, {source, 30U}}, - {"10", 1, -1.0e16, {source, 31U}}, - {"10", 1, 1.0, {source, 32U}}}); - auto secondOrder = makeFixture( - 1U, - {}, - {}, - {{"10", 1, 1.0e16, {source, 30U}}, - {"10", 1, 1.0, {source, 31U}}, - {"10", 1, -1.0e16, {source, 32U}}}); + const std::filesystem::path source{"models/load-assembly.inp"}; + auto first_order = MakeFixture(1U, {}, {}, + {{"10", 1, 1.0e16, {source, 30U}}, + {"10", 1, -1.0e16, {source, 31U}}, + {"10", 1, 1.0, {source, 32U}}}); + auto second_order = MakeFixture(1U, {}, {}, + {{"10", 1, 1.0e16, {source, 30U}}, + {"10", 1, 1.0, {source, 31U}}, + {"10", 1, -1.0e16, {source, 32U}}}); - auto first = fesa::LoadAssembler::assembleFullNodalLoad( - *firstOrder.model, *firstOrder.dofs); - auto second = fesa::LoadAssembler::assembleFullNodalLoad( - *secondOrder.model, *secondOrder.dofs); - ASSERT_TRUE(first.HasValue()); - ASSERT_TRUE(second.HasValue()); - EXPECT_DOUBLE_EQ(first.Value()[0U], 1.0); - EXPECT_DOUBLE_EQ(second.Value()[0U], 0.0); + auto first = fesa::LoadAssembler::AssembleFullNodalLoad(*first_order.model, + *first_order.dofs); + auto second = fesa::LoadAssembler::AssembleFullNodalLoad(*second_order.model, + *second_order.dofs); + ASSERT_TRUE(first.HasValue()); + ASSERT_TRUE(second.HasValue()); + EXPECT_DOUBLE_EQ(first.Value()[0U], 1.0); + EXPECT_DOUBLE_EQ(second.Value()[0U], 0.0); } // MITC4-LOAD-001 TEST(LoadAssembly, AggregatesAllSixGlobalShellLoadComponentsInSourceOrder) { - const std::filesystem::path source{"models/shell-load-assembly.inp"}; - auto fixture = makeShellFixture( - {}, - {{"10", 1, 1.0e16, {source, 30U}}, - {"10", 1, -1.0e16, {source, 31U}}, - {"10", 1, 1.0, {source, 32U}}, - {"10", 2, 2.0, {source, 33U}}, - {"10", 3, 3.0, {source, 34U}}, - {"10", 4, 1.0e16, {source, 35U}}, - {"10", 4, -1.0e16, {source, 36U}}, - {"10", 4, 4.0, {source, 37U}}, - {"10", 5, 5.0, {source, 38U}}, - {"10", 6, 6.0, {source, 39U}}, - {"10", 6, -6.0, {source, 40U}}}); + const std::filesystem::path source{"models/shell-load-assembly.inp"}; + auto fixture = MakeShellFixture({}, {{"10", 1, 1.0e16, {source, 30U}}, + {"10", 1, -1.0e16, {source, 31U}}, + {"10", 1, 1.0, {source, 32U}}, + {"10", 2, 2.0, {source, 33U}}, + {"10", 3, 3.0, {source, 34U}}, + {"10", 4, 1.0e16, {source, 35U}}, + {"10", 4, -1.0e16, {source, 36U}}, + {"10", 4, 4.0, {source, 37U}}, + {"10", 5, 5.0, {source, 38U}}, + {"10", 6, 6.0, {source, 39U}}, + {"10", 6, -6.0, {source, 40U}}}); - const auto result = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); + const auto result = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); - ASSERT_TRUE(result.HasValue()); - ASSERT_EQ(result.Value().Size(), 24U); - EXPECT_EQ( - std::vector(result.Value().Data(), result.Value().Data() + 6U), - (std::vector{1.0, 2.0, 3.0, 4.0, 5.0, 0.0})); + ASSERT_TRUE(result.HasValue()); + ASSERT_EQ(result.Value().Size(), 24U); + EXPECT_EQ( + std::vector(result.Value().Data(), result.Value().Data() + 6U), + (std::vector{1.0, 2.0, 3.0, 4.0, 5.0, 0.0})); } // MITC4-LOAD-002 TEST(LoadAssembly, AcceptsExactlyZeroAggregateShellMoment) { - const std::filesystem::path source{"models/shell-load-assembly.inp"}; - auto fixture = makeShellFixture( - {}, - {{"10", 4, 3.0, {source, 30U}}, - {"10", 4, -3.0, {source, 31U}}, - {"10", 5, 4.0, {source, 32U}}, - {"10", 5, -4.0, {source, 33U}}, - {"10", 6, 5.0, {source, 34U}}, - {"10", 6, -5.0, {source, 35U}}}); + const std::filesystem::path source{"models/shell-load-assembly.inp"}; + auto fixture = MakeShellFixture({}, {{"10", 4, 3.0, {source, 30U}}, + {"10", 4, -3.0, {source, 31U}}, + {"10", 5, 4.0, {source, 32U}}, + {"10", 5, -4.0, {source, 33U}}, + {"10", 6, 5.0, {source, 34U}}, + {"10", 6, -5.0, {source, 35U}}}); - const auto result = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); + const auto result = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); - ASSERT_TRUE(result.HasValue()); - EXPECT_DOUBLE_EQ(result.Value()[3U], 0.0); - EXPECT_DOUBLE_EQ(result.Value()[4U], 0.0); - EXPECT_DOUBLE_EQ(result.Value()[5U], 0.0); + ASSERT_TRUE(result.HasValue()); + EXPECT_DOUBLE_EQ(result.Value()[3U], 0.0); + EXPECT_DOUBLE_EQ(result.Value()[4U], 0.0); + EXPECT_DOUBLE_EQ(result.Value()[5U], 0.0); } // MITC4-LOAD-003 TEST(LoadAssembly, EnforcesAggregateShellMomentDirectorProjectionThreshold) { - const std::filesystem::path source{"models/shell-load-assembly.inp"}; - auto acceptedFixture = makeShellFixture( - {}, - {{"10", 4, 1.0, {source, 30U}}, - {"10", 6, 1.0e-12, {source, 31U}}}); - auto rejectedFixture = makeShellFixture( - {}, - {{"10", 4, 1.0, {source, 30U}}, - {"10", 6, 2.0e-12, {source, 31U}}}); + const std::filesystem::path source{"models/shell-load-assembly.inp"}; + auto accepted_fixture = MakeShellFixture( + {}, {{"10", 4, 1.0, {source, 30U}}, {"10", 6, 1.0e-12, {source, 31U}}}); + auto rejected_fixture = MakeShellFixture( + {}, {{"10", 4, 1.0, {source, 30U}}, {"10", 6, 2.0e-12, {source, 31U}}}); - const auto accepted = fesa::LoadAssembler::assembleFullNodalLoad( - *acceptedFixture.model, *acceptedFixture.dofs); - const auto rejected = fesa::LoadAssembler::assembleFullNodalLoad( - *rejectedFixture.model, *rejectedFixture.dofs); + const auto accepted = fesa::LoadAssembler::AssembleFullNodalLoad( + *accepted_fixture.model, *accepted_fixture.dofs); + const auto rejected = fesa::LoadAssembler::AssembleFullNodalLoad( + *rejected_fixture.model, *rejected_fixture.dofs); - ASSERT_TRUE(accepted.HasValue()); - ASSERT_FALSE(rejected.HasValue()); - EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kModel); - ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ( - rejected.GetStatus().Diagnostics()[0U].code, - "unsupported-drilling-load"); - EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].keyword, "CLOAD"); - EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].entity_identity, "10"); + ASSERT_TRUE(accepted.HasValue()); + ASSERT_FALSE(rejected.HasValue()); + EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].code, + "unsupported-drilling-load"); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].keyword, "CLOAD"); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].entity_identity, "10"); } // MITC4-LOAD-004 TEST(LoadAssembly, RejectsDrillingMomentBeforeEffectiveRhsCanBeFormed) { - const std::filesystem::path source{"models/shell-load-assembly.inp"}; - auto fixture = makeShellFixture( - {{"10", 1, 1, 2.0, {source, 21U}}}, - {{"10", 6, 1.0, {source, 30U}}}); + const std::filesystem::path source{"models/shell-load-assembly.inp"}; + auto fixture = MakeShellFixture({{"10", 1, 1, 2.0, {source, 21U}}}, + {{"10", 6, 1.0, {source, 30U}}}); - const auto rejected = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); + const auto rejected = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); - ASSERT_FALSE(rejected.HasValue()); - EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kModel); - ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ( - rejected.GetStatus().Diagnostics()[0U].code, - "unsupported-drilling-load"); + ASSERT_FALSE(rejected.HasValue()); + EXPECT_EQ(rejected.GetStatus().Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(rejected.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(rejected.GetStatus().Diagnostics()[0U].code, + "unsupported-drilling-load"); } TEST(LoadAssembly, FormsNonzeroPrescribedEffectiveRhs) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto fixture = makeFixture( - 1U, - {}, - {{"10", 2, 2, 2.0, {source, 21U}}, - {"10", 5, 5, -1.0, {source, 22U}}}, - {{"10", 1, 10.0, {source, 30U}}, - {"10", 2, 900.0, {source, 31U}}, - {"10", 3, 20.0, {source, 32U}}, - {"10", 4, 30.0, {source, 33U}}, - {"10", 5, 800.0, {source, 34U}}, - {"10", 6, 40.0, {source, 35U}}}); - auto full = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); - ASSERT_TRUE(full.HasValue()); - const auto kfc = makeDenseSparse( - 4U, - 2U, - {1.0, 2.0, - 3.0, 4.0, - -2.0, 5.0, - 0.5, -1.0}); + const std::filesystem::path source{"models/load-assembly.inp"}; + auto fixture = MakeFixture( + 1U, {}, + {{"10", 2, 2, 2.0, {source, 21U}}, {"10", 5, 5, -1.0, {source, 22U}}}, + {{"10", 1, 10.0, {source, 30U}}, + {"10", 2, 900.0, {source, 31U}}, + {"10", 3, 20.0, {source, 32U}}, + {"10", 4, 30.0, {source, 33U}}, + {"10", 5, 800.0, {source, 34U}}, + {"10", 6, 40.0, {source, 35U}}}); + auto full = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); + ASSERT_TRUE(full.HasValue()); + const auto kfc = + MakeDenseSparse(4U, 2U, {1.0, 2.0, 3.0, 4.0, -2.0, 5.0, 0.5, -1.0}); - auto rhs = fesa::LoadAssembler::effectiveFreeRhs( - full.Value(), kfc, fixture.dofs->prescribedValues(), *fixture.dofs); - ASSERT_TRUE(rhs.HasValue()); - ASSERT_EQ(rhs.Value().Size(), 4U); - EXPECT_EQ( - std::vector(rhs.Value().Data(), rhs.Value().Data() + 4U), - (std::vector{10.0, 18.0, 39.0, 38.0})); + auto rhs = fesa::LoadAssembler::EffectiveFreeRhs( + full.Value(), kfc, fixture.dofs->PrescribedValues(), *fixture.dofs); + ASSERT_TRUE(rhs.HasValue()); + ASSERT_EQ(rhs.Value().Size(), 4U); + EXPECT_EQ(std::vector(rhs.Value().Data(), rhs.Value().Data() + 4U), + (std::vector{10.0, 18.0, 39.0, 38.0})); } TEST(LoadAssembly, RejectsNonfiniteOrDimensionMismatch) { - const std::filesystem::path source{"models/load-assembly.inp"}; - const double maximum = (std::numeric_limits::max)(); - auto nonfinite = makeFixture( - 1U, - {}, - {}, - {{"10", 1, std::numeric_limits::quiet_NaN(), {source, 30U}}}); - expectFailureCode( - fesa::LoadAssembler::assembleFullNodalLoad( - *nonfinite.model, *nonfinite.dofs), - "nonfinite-load-value"); + const std::filesystem::path source{"models/load-assembly.inp"}; + const double maximum = (std::numeric_limits::max)(); + auto nonfinite = MakeFixture( + 1U, {}, {}, + {{"10", 1, std::numeric_limits::quiet_NaN(), {source, 30U}}}); + ExpectFailureCode(fesa::LoadAssembler::AssembleFullNodalLoad(*nonfinite.model, + *nonfinite.dofs), + "nonfinite-load-value"); - auto overflow = makeFixture( - 1U, - {}, - {}, - {{"10", 1, maximum, {source, 30U}}, - {"10", 1, maximum, {source, 31U}}}); - expectFailureCode( - fesa::LoadAssembler::assembleFullNodalLoad( - *overflow.model, *overflow.dofs), - "nonfinite-load-accumulation"); + auto overflow = MakeFixture( + 1U, {}, {}, + {{"10", 1, maximum, {source, 30U}}, {"10", 1, maximum, {source, 31U}}}); + ExpectFailureCode(fesa::LoadAssembler::AssembleFullNodalLoad(*overflow.model, + *overflow.dofs), + "nonfinite-load-accumulation"); - auto oneNode = makeFixture( - 1U, - {}, - {{"10", 2, 2, 2.0, {source, 21U}}, - {"10", 5, 5, -1.0, {source, 22U}}}, - {}); - auto twoNodes = makeFixture(2U, {}, {}, {}); - expectFailureCode( - fesa::LoadAssembler::assembleFullNodalLoad( - *twoNodes.model, *oneNode.dofs), - "invalid-load-dimensions"); + auto one_node = MakeFixture( + 1U, {}, + {{"10", 2, 2, 2.0, {source, 21U}}, {"10", 5, 5, -1.0, {source, 22U}}}, + {}); + auto two_nodes = MakeFixture(2U, {}, {}, {}); + ExpectFailureCode(fesa::LoadAssembler::AssembleFullNodalLoad(*two_nodes.model, + *one_node.dofs), + "invalid-load-dimensions"); - const auto validKfc = makeDenseSparse(4U, 2U, std::vector(8U, 0.0)); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{5U}, - validKfc, - oneNode.dofs->prescribedValues(), - *oneNode.dofs), - "invalid-load-dimensions"); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{6U}, - makeDenseSparse(3U, 2U, std::vector(6U, 0.0)), - oneNode.dofs->prescribedValues(), - *oneNode.dofs), - "invalid-load-dimensions"); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{6U}, - makeDenseSparse(4U, 1U, std::vector(4U, 0.0)), - oneNode.dofs->prescribedValues(), - *oneNode.dofs), - "invalid-load-dimensions"); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{6U}, validKfc, fesa::Vector{1U}, *oneNode.dofs), - "invalid-load-dimensions"); + const auto valid_kfc = MakeDenseSparse(4U, 2U, std::vector(8U, 0.0)); + ExpectFailureCode(fesa::LoadAssembler::EffectiveFreeRhs( + fesa::Vector{5U}, valid_kfc, + one_node.dofs->PrescribedValues(), *one_node.dofs), + "invalid-load-dimensions"); + ExpectFailureCode(fesa::LoadAssembler::EffectiveFreeRhs( + fesa::Vector{6U}, + MakeDenseSparse(3U, 2U, std::vector(6U, 0.0)), + one_node.dofs->PrescribedValues(), *one_node.dofs), + "invalid-load-dimensions"); + ExpectFailureCode(fesa::LoadAssembler::EffectiveFreeRhs( + fesa::Vector{6U}, + MakeDenseSparse(4U, 1U, std::vector(4U, 0.0)), + one_node.dofs->PrescribedValues(), *one_node.dofs), + "invalid-load-dimensions"); + ExpectFailureCode( + fesa::LoadAssembler::EffectiveFreeRhs(fesa::Vector{6U}, valid_kfc, + fesa::Vector{1U}, *one_node.dofs), + "invalid-load-dimensions"); - fesa::Vector nonfiniteFull{6U}; - nonfiniteFull[0U] = std::numeric_limits::infinity(); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - nonfiniteFull, - validKfc, - oneNode.dofs->prescribedValues(), - *oneNode.dofs), - "nonfinite-load-value"); + fesa::Vector nonfinite_full{6U}; + nonfinite_full[0U] = std::numeric_limits::infinity(); + ExpectFailureCode(fesa::LoadAssembler::EffectiveFreeRhs( + nonfinite_full, valid_kfc, + one_node.dofs->PrescribedValues(), *one_node.dofs), + "nonfinite-load-value"); - fesa::Vector nonfinitePrescribed{2U}; - nonfinitePrescribed[0U] = std::numeric_limits::quiet_NaN(); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{6U}, validKfc, nonfinitePrescribed, *oneNode.dofs), - "nonfinite-load-value"); + fesa::Vector nonfinite_prescribed{2U}; + nonfinite_prescribed[0U] = std::numeric_limits::quiet_NaN(); + ExpectFailureCode( + fesa::LoadAssembler::EffectiveFreeRhs( + fesa::Vector{6U}, valid_kfc, nonfinite_prescribed, *one_node.dofs), + "nonfinite-load-value"); - const auto overflowingKfc = makeDenseSparse( - 4U, - 2U, - {maximum, 0.0, - 0.0, 0.0, - 0.0, 0.0, - 0.0, 0.0}); - expectFailureCode( - fesa::LoadAssembler::effectiveFreeRhs( - fesa::Vector{6U}, - overflowingKfc, - oneNode.dofs->prescribedValues(), - *oneNode.dofs), - "nonfinite-load-accumulation"); + const auto overflowing_kfc = + MakeDenseSparse(4U, 2U, {maximum, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}); + ExpectFailureCode(fesa::LoadAssembler::EffectiveFreeRhs( + fesa::Vector{6U}, overflowing_kfc, + one_node.dofs->PrescribedValues(), *one_node.dofs), + "nonfinite-load-accumulation"); } TEST(LoadAssembly, ZeroLoadsRemainZero) { - const std::filesystem::path source{"models/load-assembly.inp"}; - auto freeFixture = makeFixture( - 1U, - {}, - {}, - {{"10", 3, 0.0, {source, 30U}}}); - auto full = fesa::LoadAssembler::assembleFullNodalLoad( - *freeFixture.model, *freeFixture.dofs); - ASSERT_TRUE(full.HasValue()); - EXPECT_TRUE(std::all_of( - full.Value().Data(), - full.Value().Data() + full.Value().Size(), - [](const double value) { return value == 0.0; })); - const auto noConstrainedColumns = makeDenseSparse(6U, 0U, {}); - auto freeRhs = fesa::LoadAssembler::effectiveFreeRhs( - full.Value(), - noConstrainedColumns, - freeFixture.dofs->prescribedValues(), - *freeFixture.dofs); - ASSERT_TRUE(freeRhs.HasValue()); - EXPECT_EQ(freeRhs.Value().Size(), 6U); - EXPECT_TRUE(std::all_of( - freeRhs.Value().Data(), - freeRhs.Value().Data() + freeRhs.Value().Size(), - [](const double value) { return value == 0.0; })); + const std::filesystem::path source{"models/load-assembly.inp"}; + auto free_fixture = MakeFixture(1U, {}, {}, {{"10", 3, 0.0, {source, 30U}}}); + auto full = fesa::LoadAssembler::AssembleFullNodalLoad(*free_fixture.model, + *free_fixture.dofs); + ASSERT_TRUE(full.HasValue()); + EXPECT_TRUE(std::all_of(full.Value().Data(), + full.Value().Data() + full.Value().Size(), + [](const double value) { return value == 0.0; })); + const auto no_constrained_columns = MakeDenseSparse(6U, 0U, {}); + auto free_rhs = fesa::LoadAssembler::EffectiveFreeRhs( + full.Value(), no_constrained_columns, + free_fixture.dofs->PrescribedValues(), *free_fixture.dofs); + ASSERT_TRUE(free_rhs.HasValue()); + EXPECT_EQ(free_rhs.Value().Size(), 6U); + EXPECT_TRUE(std::all_of(free_rhs.Value().Data(), + free_rhs.Value().Data() + free_rhs.Value().Size(), + [](const double value) { return value == 0.0; })); - auto constrainedFixture = makeFixture( - 1U, - {}, - {{"10", 1, 6, 0.0, {source, 21U}}}, - {}); - auto constrainedFull = fesa::LoadAssembler::assembleFullNodalLoad( - *constrainedFixture.model, *constrainedFixture.dofs); - ASSERT_TRUE(constrainedFull.HasValue()); - const auto noFreeRows = makeDenseSparse(0U, 6U, {}); - auto constrainedRhs = fesa::LoadAssembler::effectiveFreeRhs( - constrainedFull.Value(), - noFreeRows, - constrainedFixture.dofs->prescribedValues(), - *constrainedFixture.dofs); - ASSERT_TRUE(constrainedRhs.HasValue()); - EXPECT_EQ(constrainedRhs.Value().Size(), 0U); + auto constrained_fixture = + MakeFixture(1U, {}, {{"10", 1, 6, 0.0, {source, 21U}}}, {}); + auto constrained_full = fesa::LoadAssembler::AssembleFullNodalLoad( + *constrained_fixture.model, *constrained_fixture.dofs); + ASSERT_TRUE(constrained_full.HasValue()); + const auto no_free_rows = MakeDenseSparse(0U, 6U, {}); + auto constrained_rhs = fesa::LoadAssembler::EffectiveFreeRhs( + constrained_full.Value(), no_free_rows, + constrained_fixture.dofs->PrescribedValues(), *constrained_fixture.dofs); + ASSERT_TRUE(constrained_rhs.HasValue()); + EXPECT_EQ(constrained_rhs.Value().Size(), 0U); } diff --git a/tests/unit/assembly/parallel_for_test.cpp b/tests/unit/assembly/parallel_for_test.cpp index f59f548..2b150ae 100644 --- a/tests/unit/assembly/parallel_for_test.cpp +++ b/tests/unit/assembly/parallel_for_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/assembly/parallel_for.hpp" +#include "fesa/assembly/parallel_for.h" #include @@ -14,101 +14,101 @@ namespace fesa { namespace { class ParallelForBodyError final : public std::runtime_error { -public: - using std::runtime_error::runtime_error; + public: + using std::runtime_error::runtime_error; }; -std::array, 2> parallelForBackends( - const SerialParallelFor& serial, - const TbbParallelFor& tbb) { - return {std::cref(serial), std::cref(tbb)}; +std::array, 2> ParallelForBackends( + const SerialParallelFor& serial, const TbbParallelFor& tbb) { + return {std::cref(serial), std::cref(tbb)}; } TEST(ParallelFor, ZeroOneManyExecuteExactlyOnce) { - const SerialParallelFor serial; - const TbbParallelFor tbb; + const SerialParallelFor serial; + const TbbParallelFor tbb; - for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) { - std::atomic zeroVisits{0U}; - parallelFor.execute(0U, [&zeroVisits](std::size_t) { - zeroVisits.fetch_add(1U, std::memory_order_relaxed); - }); - EXPECT_EQ(zeroVisits.load(std::memory_order_relaxed), 0U); + for (const ParallelFor& parallel_for : ParallelForBackends(serial, tbb)) { + std::atomic zero_visits{0U}; + parallel_for.Execute(0U, [&zero_visits](std::size_t) { + zero_visits.fetch_add(1U, std::memory_order_relaxed); + }); + EXPECT_EQ(zero_visits.load(std::memory_order_relaxed), 0U); - for (const std::size_t count : {1U, 257U}) { - std::vector> visits(count); - for (auto& visit : visits) { - visit.store(0U, std::memory_order_relaxed); - } - parallelFor.execute(count, [&visits](std::size_t index) { - visits[index].fetch_add(1U, std::memory_order_relaxed); - }); - for (std::size_t index = 0; index < count; ++index) { - EXPECT_EQ(visits[index].load(std::memory_order_relaxed), 1U); - } - } + for (const std::size_t count : {1U, 257U}) { + std::vector> visits(count); + for (auto& visit : visits) { + visit.store(0U, std::memory_order_relaxed); + } + parallel_for.Execute(count, [&visits](std::size_t index) { + visits[index].fetch_add(1U, std::memory_order_relaxed); + }); + for (std::size_t index = 0; index < count; ++index) { + EXPECT_EQ(visits[index].load(std::memory_order_relaxed), 1U); + } } + } } TEST(ParallelFor, SerialAndTbbProduceStableIndexedOutput) { - constexpr std::size_t count = 1024U; - std::vector> serialOutput(count); - std::vector> tbbOutput(count); - std::vector> serialVisits(count); - std::vector> tbbVisits(count); - for (std::size_t index = 0; index < count; ++index) { - serialOutput[index].store(0U, std::memory_order_relaxed); - tbbOutput[index].store(0U, std::memory_order_relaxed); - serialVisits[index].store(0U, std::memory_order_relaxed); - tbbVisits[index].store(0U, std::memory_order_relaxed); - } - const auto valueForIndex = [](std::size_t index) { - return (index + 17U) * (index + 3U); - }; + constexpr std::size_t count = 1024U; + std::vector> serial_output(count); + std::vector> tbb_output(count); + std::vector> serial_visits(count); + std::vector> tbb_visits(count); + for (std::size_t index = 0; index < count; ++index) { + serial_output[index].store(0U, std::memory_order_relaxed); + tbb_output[index].store(0U, std::memory_order_relaxed); + serial_visits[index].store(0U, std::memory_order_relaxed); + tbb_visits[index].store(0U, std::memory_order_relaxed); + } + const auto value_for_index = [](std::size_t index) { + return (index + 17U) * (index + 3U); + }; - const SerialParallelFor serial; - serial.execute(count, [&serialOutput, &serialVisits, &valueForIndex](std::size_t index) { - serialOutput[index].store(valueForIndex(index), std::memory_order_relaxed); - serialVisits[index].fetch_add(1U, std::memory_order_relaxed); - }); + const SerialParallelFor serial; + serial.Execute(count, [&serial_output, &serial_visits, + &value_for_index](std::size_t index) { + serial_output[index].store(value_for_index(index), + std::memory_order_relaxed); + serial_visits[index].fetch_add(1U, std::memory_order_relaxed); + }); - const TbbParallelFor tbb; - tbb.execute(count, [&tbbOutput, &tbbVisits, &valueForIndex](std::size_t index) { - tbbOutput[index].store(valueForIndex(index), std::memory_order_relaxed); - tbbVisits[index].fetch_add(1U, std::memory_order_relaxed); - }); + const TbbParallelFor tbb; + tbb.Execute(count, [&tbb_output, &tbb_visits, + &value_for_index](std::size_t index) { + tbb_output[index].store(value_for_index(index), std::memory_order_relaxed); + tbb_visits[index].fetch_add(1U, std::memory_order_relaxed); + }); - for (std::size_t index = 0; index < count; ++index) { - EXPECT_EQ(serialVisits[index].load(std::memory_order_relaxed), 1U); - EXPECT_EQ(tbbVisits[index].load(std::memory_order_relaxed), 1U); - EXPECT_EQ( - tbbOutput[index].load(std::memory_order_relaxed), - serialOutput[index].load(std::memory_order_relaxed)); - EXPECT_EQ( - tbbOutput[index].load(std::memory_order_relaxed), - valueForIndex(index)); - } + for (std::size_t index = 0; index < count; ++index) { + EXPECT_EQ(serial_visits[index].load(std::memory_order_relaxed), 1U); + EXPECT_EQ(tbb_visits[index].load(std::memory_order_relaxed), 1U); + EXPECT_EQ(tbb_output[index].load(std::memory_order_relaxed), + serial_output[index].load(std::memory_order_relaxed)); + EXPECT_EQ(tbb_output[index].load(std::memory_order_relaxed), + value_for_index(index)); + } } TEST(ParallelFor, PropagatesBodyExceptionByContract) { - const SerialParallelFor serial; - const TbbParallelFor tbb; + const SerialParallelFor serial; + const TbbParallelFor tbb; - for (const ParallelFor& parallelFor : parallelForBackends(serial, tbb)) { - try { - // Every iteration throws the same value so the assertion is independent - // of which oneTBB task reports the cancellation-triggering exception. - parallelFor.execute(64U, [](std::size_t) { - throw ParallelForBodyError{"parallel-for-body-failure"}; - }); - ADD_FAILURE() << "ParallelFor swallowed the body exception."; - } catch (const ParallelForBodyError& error) { - EXPECT_EQ(std::string{error.what()}, "parallel-for-body-failure"); - } catch (...) { - ADD_FAILURE() << "ParallelFor changed the body exception type."; - } + for (const ParallelFor& parallel_for : ParallelForBackends(serial, tbb)) { + try { + // Every iteration throws the same value so the assertion is independent + // of which oneTBB task reports the cancellation-triggering exception. + parallel_for.Execute(64U, [](std::size_t) { + throw ParallelForBodyError{"parallel-for-body-failure"}; + }); + ADD_FAILURE() << "ParallelFor swallowed the body exception."; + } catch (const ParallelForBodyError& error) { + EXPECT_EQ(std::string{error.what()}, "parallel-for-body-failure"); + } catch (...) { + ADD_FAILURE() << "ParallelFor changed the body exception type."; } + } } -} // namespace -} // namespace fesa +} // namespace +} // namespace fesa diff --git a/tests/unit/assembly/sparse_assembler_test.cpp b/tests/unit/assembly/sparse_assembler_test.cpp index b21cb3c..dabd125 100644 --- a/tests/unit/assembly/sparse_assembler_test.cpp +++ b/tests/unit/assembly/sparse_assembler_test.cpp @@ -1,9 +1,4 @@ -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/assembly/parallel_for.hpp" -#include "fesa/assembly/sparse_assembler.hpp" -#include "fesa/elements/mitc4_shell.h" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/model/domain.h" +#include "fesa/assembly/sparse_assembler.h" #include @@ -14,326 +9,311 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/assembly/parallel_for.h" +#include "fesa/elements/mitc4_shell.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + namespace { -fesa::ModelDefinition makeDefinition() { - const std::filesystem::path source{"models/sparse-assembly.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Beam-1", 2, "2"}, {2.0, 0.0, 0.0}, {source, 11U}}, - {{"Beam-1", 3, "3"}, {5.0, 0.0, 0.0}, {source, 12U}}}; - definition.materials = { - {"Material", 120.0, 0.25, {source, 20U}}}; - definition.sections = {{ - "Section", - 2.0, - 1.5, - 0.0, - 0.75, - 0.5, - {0.0, 1.0, 0.0}, - {}, - {source, 30U}}}; - definition.elements = { - {{"Beam-1", 10, "10"}, {0U, 1U}, 0U, 0U, {source, 40U}}, - {{"Beam-1", 20, "20"}, {1U, 2U}, 0U, 0U, {source, 41U}}}; - definition.steps = {{ - "Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; - return definition; +fesa::ModelDefinition MakeDefinition() { + const std::filesystem::path source{"models/sparse-assembly.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = {{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Beam-1", 2, "2"}, {2.0, 0.0, 0.0}, {source, 11U}}, + {{"Beam-1", 3, "3"}, {5.0, 0.0, 0.0}, {source, 12U}}}; + definition.materials = {{"Material", 120.0, 0.25, {source, 20U}}}; + definition.sections = {{"Section", + 2.0, + 1.5, + 0.0, + 0.75, + 0.5, + {0.0, 1.0, 0.0}, + {}, + {source, 30U}}}; + definition.elements = { + {{"Beam-1", 10, "10"}, {0U, 1U}, 0U, 0U, {source, 40U}}, + {{"Beam-1", 20, "20"}, {1U, 2U}, 0U, 0U, {source, 41U}}}; + definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; + return definition; } -fesa::ModelDefinition makeShellDefinition( - const fesa::ShellSourceElementType sourceType, - const bool twoElements = false) { - const std::filesystem::path source{"models/shell-sparse-assembly.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:fedcba9876543210"; - if (twoElements) { - definition.nodes = { - {{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Shell-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 11U}}, - {{"Shell-1", 3, "3"}, {2.0, 0.0, 0.0}, {source, 12U}}, - {{"Shell-1", 4, "4"}, {0.0, 1.0, 0.0}, {source, 13U}}, - {{"Shell-1", 5, "5"}, {1.0, 1.0, 0.0}, {source, 14U}}, - {{"Shell-1", 6, "6"}, {2.0, 1.0, 0.0}, {source, 15U}}}; - 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.shell_elements = { - {{"Shell-1", 10, "10"}, sourceType, {0U, 1U, 4U, 3U}, - 0U, 0U, {source, 40U}}, - {{"Shell-1", 20, "20"}, sourceType, {1U, 2U, 5U, 4U}, - 0U, 0U, {source, 41U}}}; - } else { - // A YZ-plane fixture catches any accidental global-Z director assumption. - definition.nodes = { - {{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Shell-1", 2, "2"}, {0.0, 1.0, 0.0}, {source, 11U}}, - {{"Shell-1", 3, "3"}, {0.0, 1.0, 1.0}, {source, 12U}}, - {{"Shell-1", 4, "4"}, {0.0, 0.0, 1.0}, {source, 13U}}}; - for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { - definition.shell_node_initial_frames.push_back({ - static_cast(node), - {1.0, 0.0, 0.0}, - {0.0, 1.0, 0.0}, - {0.0, 0.0, 1.0}}); - } - definition.shell_elements = {{ - {"Shell-1", 10, "10"}, sourceType, {0U, 1U, 2U, 3U}, - 0U, 0U, {source, 40U}}}; +fesa::ModelDefinition MakeShellDefinition( + const fesa::ShellSourceElementType source_type, + const bool two_elements = false) { + const std::filesystem::path source{"models/shell-sparse-assembly.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:fedcba9876543210"; + if (two_elements) { + definition.nodes = {{{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Shell-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 11U}}, + {{"Shell-1", 3, "3"}, {2.0, 0.0, 0.0}, {source, 12U}}, + {{"Shell-1", 4, "4"}, {0.0, 1.0, 0.0}, {source, 13U}}, + {{"Shell-1", 5, "5"}, {1.0, 1.0, 0.0}, {source, 14U}}, + {{"Shell-1", 6, "6"}, {2.0, 1.0, 0.0}, {source, 15U}}}; + 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.materials = { - {"Material", 120.0, 0.25, {source, 20U}}}; - definition.shell_sections = { - {"ShellSection", 0.2, 0U, {source, 30U}}}; - definition.steps = {{ - "Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; - return definition; + definition.shell_elements = {{{"Shell-1", 10, "10"}, + source_type, + {0U, 1U, 4U, 3U}, + 0U, + 0U, + {source, 40U}}, + {{"Shell-1", 20, "20"}, + source_type, + {1U, 2U, 5U, 4U}, + 0U, + 0U, + {source, 41U}}}; + } else { + // A YZ-plane fixture catches any accidental global-Z director assumption. + definition.nodes = {{{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Shell-1", 2, "2"}, {0.0, 1.0, 0.0}, {source, 11U}}, + {{"Shell-1", 3, "3"}, {0.0, 1.0, 1.0}, {source, 12U}}, + {{"Shell-1", 4, "4"}, {0.0, 0.0, 1.0}, {source, 13U}}}; + for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { + definition.shell_node_initial_frames.push_back( + {static_cast(node), + {1.0, 0.0, 0.0}, + {0.0, 1.0, 0.0}, + {0.0, 0.0, 1.0}}); + } + definition.shell_elements = {{{"Shell-1", 10, "10"}, + source_type, + {0U, 1U, 2U, 3U}, + 0U, + 0U, + {source, 40U}}}; + } + definition.materials = {{"Material", 120.0, 0.25, {source, 20U}}}; + definition.shell_sections = {{"ShellSection", 0.2, 0U, {source, 30U}}}; + definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; + return definition; } -fesa::Result directShellStiffness( - const fesa::Domain& domain, - const fesa::EntityIndex elementIndex) { - const auto& definition = domain.ShellElements().at(elementIndex); - std::array nodes{}; - std::array, 4> directors{}; - for (std::size_t node = 0U; node < definition.node_indices.size(); ++node) { - const fesa::EntityIndex nodeIndex = definition.node_indices[node]; - nodes[node] = &domain.Nodes().at(nodeIndex); - directors[node] = domain.ShellNodeInitialFrames().at(nodeIndex).director; - } - auto shell = fesa::Mitc4Shell::Create( - nodes, - directors, - domain.ShellSections().at(definition.section_index), - domain.Materials().at(definition.material_index)); - if (!shell.HasValue()) { - return fesa::Result::Failure(shell.GetStatus()); - } - return shell.Value().Stiffness(); +fesa::Result DirectShellStiffness( + const fesa::Domain& domain, const fesa::EntityIndex element_index) { + const auto& definition = domain.ShellElements().at(element_index); + std::array nodes{}; + std::array, 4> directors{}; + for (std::size_t node = 0U; node < definition.node_indices.size(); ++node) { + const fesa::EntityIndex node_index = definition.node_indices[node]; + nodes[node] = &domain.Nodes().at(node_index); + directors[node] = domain.ShellNodeInitialFrames().at(node_index).director; + } + auto shell = fesa::Mitc4Shell::Create( + nodes, directors, domain.ShellSections().at(definition.section_index), + domain.Materials().at(definition.material_index)); + if (!shell.HasValue()) { + return fesa::Result::Failure(shell.GetStatus()); + } + return shell.Value().Stiffness(); } -fesa::Result assembleShell( - const fesa::ShellSourceElementType sourceType, - const fesa::ParallelFor& parallelFor, - const bool twoElements = false) { - auto domain = fesa::Domain::Create( - makeShellDefinition(sourceType, twoElements)); - if (!domain.HasValue()) { - return fesa::Result::Failure(domain.GetStatus()); - } - auto model = fesa::AnalysisModel::create(domain.Value()); - if (!model.HasValue()) { - return fesa::Result::Failure(model.GetStatus()); - } - auto dofs = fesa::DofManager::create(model.Value()); - if (!dofs.HasValue()) { - return fesa::Result::Failure(dofs.GetStatus()); - } - return fesa::SparseAssembler::assembleStiffness( - model.Value(), dofs.Value(), parallelFor); +fesa::Result AssembleShell( + const fesa::ShellSourceElementType source_type, + const fesa::ParallelFor& parallel_for, const bool two_elements = false) { + auto domain = + fesa::Domain::Create(MakeShellDefinition(source_type, two_elements)); + if (!domain.HasValue()) { + return fesa::Result::Failure(domain.GetStatus()); + } + auto model = fesa::AnalysisModel::Create(domain.Value()); + if (!model.HasValue()) { + return fesa::Result::Failure(model.GetStatus()); + } + auto dofs = fesa::DofManager::Create(model.Value()); + if (!dofs.HasValue()) { + return fesa::Result::Failure(dofs.GetStatus()); + } + return fesa::SparseAssembler::AssembleStiffness(model.Value(), dofs.Value(), + parallel_for); } -template -bool byteIdentical(const std::vector& left, const std::vector& right) { - return left.size() == right.size() && - (left.empty() || - std::memcmp( - left.data(), right.data(), left.size() * sizeof(T)) == 0); +template +bool ByteIdentical(const std::vector& left, const std::vector& right) { + return left.size() == right.size() && + (left.empty() || + std::memcmp(left.data(), right.data(), left.size() * sizeof(T)) == 0); } -double entry( - const fesa::SparseMatrix& matrix, - const std::size_t row, - const std::size_t column) { - const auto begin = matrix.ColumnIndices().begin() + matrix.RowOffsets()[row]; - const auto end = matrix.ColumnIndices().begin() + matrix.RowOffsets()[row + 1U]; - const auto found = std::lower_bound(begin, end, column); - if (found == end || *found != column) { - return 0.0; - } - return matrix.Values()[static_cast( - std::distance(matrix.ColumnIndices().begin(), found))]; +double Entry(const fesa::SparseMatrix& matrix, const std::size_t row, + const std::size_t column) { + const auto begin = matrix.ColumnIndices().begin() + matrix.RowOffsets()[row]; + const auto end = + matrix.ColumnIndices().begin() + matrix.RowOffsets()[row + 1U]; + const auto found = std::lower_bound(begin, end, column); + if (found == end || *found != column) { + return 0.0; + } + return matrix.Values()[static_cast( + std::distance(matrix.ColumnIndices().begin(), found))]; } class ReverseParallelFor final : public fesa::ParallelFor { -public: - void execute( - const std::size_t count, - const std::function& body) const override { - ++calls_; - observedCount_ = count; - for (std::size_t index = count; index > 0U; --index) { - body(index - 1U); - } + public: + void Execute(const std::size_t count, + const std::function& body) const override { + ++calls_; + observed_count_ = count; + for (std::size_t index = count; index > 0U; --index) { + body(index - 1U); } + } - std::size_t calls() const noexcept { - return calls_; - } + std::size_t Calls() const noexcept { return calls_; } - std::size_t observedCount() const noexcept { - return observedCount_; - } + std::size_t ObservedCount() const noexcept { return observed_count_; } -private: - mutable std::size_t calls_{0U}; - mutable std::size_t observedCount_{0U}; + private: + mutable std::size_t calls_{0U}; + mutable std::size_t observed_count_{0U}; }; -void expectByteIdentical( - const fesa::SparseMatrix& actual, - const fesa::SparseMatrix& expected) { - EXPECT_TRUE(byteIdentical(actual.RowOffsets(), expected.RowOffsets())); - EXPECT_TRUE(byteIdentical(actual.ColumnIndices(), expected.ColumnIndices())); - EXPECT_TRUE(byteIdentical(actual.Values(), expected.Values())); +void ExpectByteIdentical(const fesa::SparseMatrix& actual, + const fesa::SparseMatrix& expected) { + EXPECT_TRUE(ByteIdentical(actual.RowOffsets(), expected.RowOffsets())); + EXPECT_TRUE(ByteIdentical(actual.ColumnIndices(), expected.ColumnIndices())); + EXPECT_TRUE(ByteIdentical(actual.Values(), expected.Values())); } TEST(SparseAssembly, SerialTbbAndRepeatedRunsAreByteIdentical) { - auto domainResult = fesa::Domain::Create(makeDefinition()); - ASSERT_TRUE(domainResult.HasValue()); - auto modelResult = fesa::AnalysisModel::create(domainResult.Value()); - ASSERT_TRUE(modelResult.HasValue()); - auto dofsResult = fesa::DofManager::create(modelResult.Value()); - ASSERT_TRUE(dofsResult.HasValue()); + auto domain_result = fesa::Domain::Create(MakeDefinition()); + ASSERT_TRUE(domain_result.HasValue()); + auto model_result = fesa::AnalysisModel::Create(domain_result.Value()); + ASSERT_TRUE(model_result.HasValue()); + auto dofs_result = fesa::DofManager::Create(model_result.Value()); + ASSERT_TRUE(dofs_result.HasValue()); - fesa::SerialParallelFor serialExecutor; - fesa::TbbParallelFor tbbExecutor; - ReverseParallelFor reverseExecutor; - auto serial = fesa::SparseAssembler::assembleStiffness( - modelResult.Value(), dofsResult.Value(), serialExecutor); - auto tbb = fesa::SparseAssembler::assembleStiffness( - modelResult.Value(), dofsResult.Value(), tbbExecutor); - auto reversed = fesa::SparseAssembler::assembleStiffness( - modelResult.Value(), dofsResult.Value(), reverseExecutor); - ASSERT_TRUE(serial.HasValue()); - ASSERT_TRUE(tbb.HasValue()); - ASSERT_TRUE(reversed.HasValue()); + fesa::SerialParallelFor serial_executor; + fesa::TbbParallelFor tbb_executor; + ReverseParallelFor reverse_executor; + auto serial = fesa::SparseAssembler::AssembleStiffness( + model_result.Value(), dofs_result.Value(), serial_executor); + auto tbb = fesa::SparseAssembler::AssembleStiffness( + model_result.Value(), dofs_result.Value(), tbb_executor); + auto reversed = fesa::SparseAssembler::AssembleStiffness( + model_result.Value(), dofs_result.Value(), reverse_executor); + ASSERT_TRUE(serial.HasValue()); + ASSERT_TRUE(tbb.HasValue()); + ASSERT_TRUE(reversed.HasValue()); - EXPECT_EQ(reverseExecutor.calls(), 1U); - EXPECT_EQ(reverseExecutor.observedCount(), 2U); - EXPECT_EQ(serial.Value().Rows(), 18U); - EXPECT_EQ(serial.Value().Columns(), 18U); - EXPECT_EQ(serial.Value().RowOffsets(), dofsResult.Value().sparsePattern().rowOffsets); - EXPECT_EQ( - serial.Value().ColumnIndices(), - dofsResult.Value().sparsePattern().columnIndices); - EXPECT_TRUE(serial.Value().Validate().IsOk()); - expectByteIdentical(tbb.Value(), serial.Value()); - expectByteIdentical(reversed.Value(), serial.Value()); + EXPECT_EQ(reverse_executor.Calls(), 1U); + EXPECT_EQ(reverse_executor.ObservedCount(), 2U); + EXPECT_EQ(serial.Value().Rows(), 18U); + EXPECT_EQ(serial.Value().Columns(), 18U); + EXPECT_EQ(serial.Value().RowOffsets(), + dofs_result.Value().GetSparsePattern().row_offsets); + EXPECT_EQ(serial.Value().ColumnIndices(), + dofs_result.Value().GetSparsePattern().column_indices); + EXPECT_TRUE(serial.Value().Validate().IsOk()); + ExpectByteIdentical(tbb.Value(), serial.Value()); + ExpectByteIdentical(reversed.Value(), serial.Value()); - for (std::size_t repetition = 0U; repetition < 8U; ++repetition) { - auto repeated = fesa::SparseAssembler::assembleStiffness( - modelResult.Value(), dofsResult.Value(), tbbExecutor); - ASSERT_TRUE(repeated.HasValue()); - expectByteIdentical(repeated.Value(), serial.Value()); + for (std::size_t repetition = 0U; repetition < 8U; ++repetition) { + auto repeated = fesa::SparseAssembler::AssembleStiffness( + model_result.Value(), dofs_result.Value(), tbb_executor); + ASSERT_TRUE(repeated.HasValue()); + ExpectByteIdentical(repeated.Value(), serial.Value()); + } + + for (std::size_t row = 0U; row < serial.Value().Rows(); ++row) { + for (std::size_t column = 0U; column < serial.Value().Columns(); ++column) { + EXPECT_DOUBLE_EQ(Entry(serial.Value(), row, column), + Entry(serial.Value(), column, row)); } + } - for (std::size_t row = 0U; row < serial.Value().Rows(); ++row) { - for (std::size_t column = 0U; - column < serial.Value().Columns(); - ++column) { - EXPECT_DOUBLE_EQ( - entry(serial.Value(), row, column), - entry(serial.Value(), column, row)); - } - } - - EXPECT_NEAR(entry(serial.Value(), 0U, 0U), 120.0, 1.0e-12); - EXPECT_NEAR(entry(serial.Value(), 0U, 6U), -120.0, 1.0e-12); - EXPECT_NEAR(entry(serial.Value(), 6U, 6U), 200.0, 1.0e-12); - EXPECT_NEAR(entry(serial.Value(), 6U, 12U), -80.0, 1.0e-12); - EXPECT_NEAR(entry(serial.Value(), 12U, 12U), 80.0, 1.0e-12); + EXPECT_NEAR(Entry(serial.Value(), 0U, 0U), 120.0, 1.0e-12); + EXPECT_NEAR(Entry(serial.Value(), 0U, 6U), -120.0, 1.0e-12); + EXPECT_NEAR(Entry(serial.Value(), 6U, 6U), 200.0, 1.0e-12); + EXPECT_NEAR(Entry(serial.Value(), 6U, 12U), -80.0, 1.0e-12); + EXPECT_NEAR(Entry(serial.Value(), 12U, 12U), 80.0, 1.0e-12); } -TEST( - SparseAssembly, - AssemblesFourNodeTwentyFourDofKernelAndPreservesDiagonalSlots) { - auto domain = fesa::Domain::Create( - makeShellDefinition(fesa::ShellSourceElementType::kS4)); - ASSERT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - ASSERT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - ASSERT_TRUE(dofs.HasValue()); - fesa::SerialParallelFor serialExecutor; +TEST(SparseAssembly, + AssemblesFourNodeTwentyFourDofKernelAndPreservesDiagonalSlots) { + auto domain = fesa::Domain::Create( + MakeShellDefinition(fesa::ShellSourceElementType::kS4)); + ASSERT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + ASSERT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + ASSERT_TRUE(dofs.HasValue()); + fesa::SerialParallelFor serial_executor; - auto assembled = fesa::SparseAssembler::assembleStiffness( - model.Value(), dofs.Value(), serialExecutor); - auto expected = directShellStiffness(domain.Value(), 0U); - ASSERT_TRUE(assembled.HasValue()); - ASSERT_TRUE(expected.HasValue()); + auto assembled = fesa::SparseAssembler::AssembleStiffness( + model.Value(), dofs.Value(), serial_executor); + auto expected = DirectShellStiffness(domain.Value(), 0U); + ASSERT_TRUE(assembled.HasValue()); + ASSERT_TRUE(expected.HasValue()); - EXPECT_EQ(assembled.Value().Rows(), 24U); - EXPECT_EQ(assembled.Value().Columns(), 24U); - EXPECT_EQ(assembled.Value().Values().size(), 24U * 24U); - EXPECT_EQ( - assembled.Value().RowOffsets(), - dofs.Value().sparsePattern().rowOffsets); - EXPECT_EQ( - assembled.Value().ColumnIndices(), - dofs.Value().sparsePattern().columnIndices); - for (std::size_t row = 0U; row < 24U; ++row) { - const auto begin = assembled.Value().ColumnIndices().begin() + - assembled.Value().RowOffsets()[row]; - const auto end = assembled.Value().ColumnIndices().begin() + - assembled.Value().RowOffsets()[row + 1U]; - EXPECT_NE(std::lower_bound(begin, end, row), end); - for (std::size_t column = 0U; column < 24U; ++column) { - EXPECT_DOUBLE_EQ( - entry(assembled.Value(), row, column), - expected.Value().stabilized_global24(row, column)); - } + EXPECT_EQ(assembled.Value().Rows(), 24U); + EXPECT_EQ(assembled.Value().Columns(), 24U); + EXPECT_EQ(assembled.Value().Values().size(), 24U * 24U); + EXPECT_EQ(assembled.Value().RowOffsets(), + dofs.Value().GetSparsePattern().row_offsets); + EXPECT_EQ(assembled.Value().ColumnIndices(), + dofs.Value().GetSparsePattern().column_indices); + for (std::size_t row = 0U; row < 24U; ++row) { + const auto begin = assembled.Value().ColumnIndices().begin() + + assembled.Value().RowOffsets()[row]; + const auto end = assembled.Value().ColumnIndices().begin() + + assembled.Value().RowOffsets()[row + 1U]; + EXPECT_NE(std::lower_bound(begin, end, row), end); + for (std::size_t column = 0U; column < 24U; ++column) { + EXPECT_DOUBLE_EQ(Entry(assembled.Value(), row, column), + expected.Value().stabilized_global24(row, column)); } + } } TEST(SparseAssembly, ShellSerialTbbReverseAndRepeatedRunsAreByteIdentical) { - fesa::SerialParallelFor serialExecutor; - fesa::TbbParallelFor tbbExecutor; - ReverseParallelFor reverseExecutor; - auto serial = assembleShell( - fesa::ShellSourceElementType::kS4, serialExecutor, true); - auto tbb = assembleShell( - fesa::ShellSourceElementType::kS4, tbbExecutor, true); - auto reversed = assembleShell( - fesa::ShellSourceElementType::kS4, reverseExecutor, true); - ASSERT_TRUE(serial.HasValue()); - ASSERT_TRUE(tbb.HasValue()); - ASSERT_TRUE(reversed.HasValue()); + fesa::SerialParallelFor serial_executor; + fesa::TbbParallelFor tbb_executor; + ReverseParallelFor reverse_executor; + auto serial = + AssembleShell(fesa::ShellSourceElementType::kS4, serial_executor, true); + auto tbb = + AssembleShell(fesa::ShellSourceElementType::kS4, tbb_executor, true); + auto reversed = + AssembleShell(fesa::ShellSourceElementType::kS4, reverse_executor, true); + ASSERT_TRUE(serial.HasValue()); + ASSERT_TRUE(tbb.HasValue()); + ASSERT_TRUE(reversed.HasValue()); - EXPECT_EQ(reverseExecutor.calls(), 1U); - EXPECT_EQ(reverseExecutor.observedCount(), 2U); - expectByteIdentical(tbb.Value(), serial.Value()); - expectByteIdentical(reversed.Value(), serial.Value()); - for (std::size_t repetition = 0U; repetition < 8U; ++repetition) { - auto repeated = assembleShell( - fesa::ShellSourceElementType::kS4, tbbExecutor, true); - ASSERT_TRUE(repeated.HasValue()); - expectByteIdentical(repeated.Value(), serial.Value()); - } + EXPECT_EQ(reverse_executor.Calls(), 1U); + EXPECT_EQ(reverse_executor.ObservedCount(), 2U); + ExpectByteIdentical(tbb.Value(), serial.Value()); + ExpectByteIdentical(reversed.Value(), serial.Value()); + for (std::size_t repetition = 0U; repetition < 8U; ++repetition) { + auto repeated = + AssembleShell(fesa::ShellSourceElementType::kS4, tbb_executor, true); + ASSERT_TRUE(repeated.HasValue()); + ExpectByteIdentical(repeated.Value(), serial.Value()); + } } TEST(SparseAssembly, S4AndS4rSemanticFixturesAssembleIdenticalStiffness) { - fesa::SerialParallelFor serialExecutor; - auto s4 = assembleShell( - fesa::ShellSourceElementType::kS4, serialExecutor); - auto s4r = assembleShell( - fesa::ShellSourceElementType::kS4r, serialExecutor); - ASSERT_TRUE(s4.HasValue()); - ASSERT_TRUE(s4r.HasValue()); - EXPECT_TRUE(std::any_of( - s4.Value().Values().begin(), - s4.Value().Values().end(), - [](const double value) { return value != 0.0; })); - expectByteIdentical(s4r.Value(), s4.Value()); + fesa::SerialParallelFor serial_executor; + auto s4 = AssembleShell(fesa::ShellSourceElementType::kS4, serial_executor); + auto s4r = AssembleShell(fesa::ShellSourceElementType::kS4r, serial_executor); + ASSERT_TRUE(s4.HasValue()); + ASSERT_TRUE(s4r.HasValue()); + EXPECT_TRUE(std::any_of(s4.Value().Values().begin(), + s4.Value().Values().end(), + [](const double value) { return value != 0.0; })); + ExpectByteIdentical(s4r.Value(), s4.Value()); } -} // namespace +} // namespace diff --git a/tests/unit/constraints/essential_constraints_test.cpp b/tests/unit/constraints/essential_constraints_test.cpp index d7fcb8f..5e03c6c 100644 --- a/tests/unit/constraints/essential_constraints_test.cpp +++ b/tests/unit/constraints/essential_constraints_test.cpp @@ -1,8 +1,4 @@ -#include "fesa/constraints/essential_constraints.hpp" - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/model/domain.h" +#include "fesa/constraints/essential_constraints.h" #include @@ -13,339 +9,307 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + namespace { -fesa::DofManager makeDofs( +fesa::DofManager MakeDofs(std::vector boundaries) { + const std::filesystem::path source{"models/essential-constraints.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:1234567890abcdef"; + definition.nodes = {{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 2U}}}; + definition.steps = {{"Step-1", + std::move(boundaries), + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 10U}}}; + + auto domain = fesa::Domain::Create(std::move(definition)); + EXPECT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + EXPECT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + EXPECT_TRUE(dofs.HasValue()); + return std::move(dofs.Value()); +} + +fesa::DofManager MakeShellSizedDofs( std::vector boundaries) { - const std::filesystem::path source{"models/essential-constraints.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:1234567890abcdef"; - definition.nodes = { - {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 2U}}}; - definition.steps = {{ - "Step-1", - std::move(boundaries), - {}, - 0.1, - 1.0, - 0.01, - 1.0, - {source, 10U}}}; + const std::filesystem::path source{"models/shell-essential-constraints.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:8877665544332211"; + definition.nodes = {{{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 2U}}, + {{"Shell-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 3U}}, + {{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {source, 4U}}, + {{"Shell-1", 4, "4"}, {0.0, 1.0, 0.0}, {source, 5U}}}; + definition.materials = {{"Material", 1000.0, 0.25, {source, 6U}}}; + definition.shell_sections = {{"ShellSection", 0.1, 0U, {source, 7U}}}; + definition.shell_elements = {{{"Shell-1", 1, "1"}, + fesa::ShellSourceElementType::kS4, + {0U, 1U, 2U, 3U}, + 0U, + 0U, + {source, 8U}}}; + definition.steps = {{"Step-1", + std::move(boundaries), + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 10U}}}; - auto domain = fesa::Domain::Create(std::move(definition)); - EXPECT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - EXPECT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - EXPECT_TRUE(dofs.HasValue()); - return std::move(dofs.Value()); + auto domain = fesa::Domain::Create(std::move(definition)); + EXPECT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + EXPECT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + EXPECT_TRUE(dofs.HasValue()); + return std::move(dofs.Value()); } -fesa::DofManager makeShellSizedDofs( - std::vector boundaries) { - const std::filesystem::path source{"models/shell-essential-constraints.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:8877665544332211"; - definition.nodes = { - {{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 2U}}, - {{"Shell-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 3U}}, - {{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {source, 4U}}, - {{"Shell-1", 4, "4"}, {0.0, 1.0, 0.0}, {source, 5U}}}; - definition.materials = { - {"Material", 1000.0, 0.25, {source, 6U}}}; - definition.shell_sections = { - {"ShellSection", 0.1, 0U, {source, 7U}}}; - definition.shell_elements = {{ - {"Shell-1", 1, "1"}, - fesa::ShellSourceElementType::kS4, - {0U, 1U, 2U, 3U}, - 0U, - 0U, - {source, 8U}}}; - definition.steps = {{ - "Step-1", - std::move(boundaries), - {}, - 0.1, - 1.0, - 0.01, - 1.0, - {source, 10U}}}; - - auto domain = fesa::Domain::Create(std::move(definition)); - EXPECT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - EXPECT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - EXPECT_TRUE(dofs.HasValue()); - return std::move(dofs.Value()); -} - -fesa::SparseMatrix makeMatrix( - const std::size_t rows, - const std::size_t columns, - const std::vector& denseValues) { - EXPECT_EQ(denseValues.size(), rows * columns); - fesa::SparsePattern pattern; - std::vector contributions; - pattern.rowOffsets.reserve(rows + 1U); - pattern.rowOffsets.push_back(0U); - for (std::size_t row = 0U; row < rows; ++row) { - for (std::size_t column = 0U; column < columns; ++column) { - pattern.columnIndices.push_back(column); - contributions.push_back({ - row, - column, - denseValues[row * columns + column], - row, - column}); - } - pattern.rowOffsets.push_back(pattern.columnIndices.size()); +fesa::SparseMatrix MakeMatrix(const std::size_t rows, const std::size_t columns, + const std::vector& dense_values) { + EXPECT_EQ(dense_values.size(), rows * columns); + fesa::SparsePattern pattern; + std::vector contributions; + pattern.row_offsets.reserve(rows + 1U); + pattern.row_offsets.push_back(0U); + for (std::size_t row = 0U; row < rows; ++row) { + for (std::size_t column = 0U; column < columns; ++column) { + pattern.column_indices.push_back(column); + contributions.push_back( + {row, column, dense_values[row * columns + column], row, column}); } + pattern.row_offsets.push_back(pattern.column_indices.size()); + } - auto matrix = fesa::SparseMatrix::FromCoo( - rows, columns, std::move(contributions), pattern); - EXPECT_TRUE(matrix.HasValue()); - return std::move(matrix.Value()); + auto matrix = fesa::SparseMatrix::FromCoo(rows, columns, + std::move(contributions), pattern); + EXPECT_TRUE(matrix.HasValue()); + return std::move(matrix.Value()); } -std::vector sequentialDense(const std::size_t size) { - std::vector values(size * size); - for (std::size_t row = 0U; row < size; ++row) { - for (std::size_t column = 0U; column < size; ++column) { - values[row * size + column] = - static_cast(row * 10U + column + 1U); - } +std::vector SequentialDense(const std::size_t size) { + std::vector values(size * size); + for (std::size_t row = 0U; row < size; ++row) { + for (std::size_t column = 0U; column < size; ++column) { + values[row * size + column] = + static_cast(row * 10U + column + 1U); } - return values; + } + return values; } -void expectShape( - const fesa::SparseMatrix& matrix, - const std::size_t rows, - const std::size_t columns) { - EXPECT_EQ(matrix.Rows(), rows); - EXPECT_EQ(matrix.Columns(), columns); - EXPECT_TRUE(matrix.Validate().IsOk()); +void ExpectShape(const fesa::SparseMatrix& matrix, const std::size_t rows, + const std::size_t columns) { + EXPECT_EQ(matrix.Rows(), rows); + EXPECT_EQ(matrix.Columns(), columns); + EXPECT_TRUE(matrix.Validate().IsOk()); } -} // namespace +} // namespace TEST(EssentialConstraints, ExtractsHandComputedBlocksInStableOrder) { - const auto dofs = makeDofs({ - {"1", 2, 2, 2.5, {{}, 12U}}, - {"1", 5, 5, -3.25, {{}, 13U}}}); - auto fullValues = sequentialDense(6U); - fullValues[2U * 6U + 4U] = 0.0; - const auto full = makeMatrix(6U, 6U, fullValues); + const auto dofs = + MakeDofs({{"1", 2, 2, 2.5, {{}, 12U}}, {"1", 5, 5, -3.25, {{}, 13U}}}); + auto full_values = SequentialDense(6U); + full_values[2U * 6U + 4U] = 0.0; + const auto full = MakeMatrix(6U, 6U, full_values); - auto result = fesa::EssentialConstraints::partition(full, dofs); - ASSERT_TRUE(result.HasValue()); - const auto& blocks = result.Value(); + auto result = fesa::EssentialConstraints::Partition(full, dofs); + ASSERT_TRUE(result.HasValue()); + const auto& blocks = result.Value(); - EXPECT_EQ(blocks.kff.RowOffsets(), (std::vector{0U, 4U, 8U, 12U, 16U})); - EXPECT_EQ( - blocks.kff.ColumnIndices(), - (std::vector{ - 0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U, - 0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U})); - EXPECT_EQ( - blocks.kff.Values(), - (std::vector{ - 1.0, 3.0, 4.0, 6.0, - 21.0, 23.0, 24.0, 26.0, - 31.0, 33.0, 34.0, 36.0, - 51.0, 53.0, 54.0, 56.0})); - EXPECT_EQ(blocks.kfc.RowOffsets(), (std::vector{0U, 2U, 4U, 6U, 8U})); - EXPECT_EQ( - blocks.kfc.ColumnIndices(), - (std::vector{0U, 1U, 0U, 1U, 0U, 1U, 0U, 1U})); - EXPECT_EQ( - blocks.kfc.Values(), - (std::vector{2.0, 5.0, 22.0, 0.0, 32.0, 35.0, 52.0, 55.0})); - EXPECT_EQ(blocks.kcf.RowOffsets(), (std::vector{0U, 4U, 8U})); - EXPECT_EQ( - blocks.kcf.ColumnIndices(), - (std::vector{0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U})); - EXPECT_EQ( - blocks.kcf.Values(), - (std::vector{11.0, 13.0, 14.0, 16.0, 41.0, 43.0, 44.0, 46.0})); - EXPECT_EQ(blocks.kcc.RowOffsets(), (std::vector{0U, 2U, 4U})); - EXPECT_EQ(blocks.kcc.ColumnIndices(), (std::vector{0U, 1U, 0U, 1U})); - EXPECT_EQ(blocks.kcc.Values(), (std::vector{12.0, 15.0, 42.0, 45.0})); + EXPECT_EQ(blocks.kff.RowOffsets(), + (std::vector{0U, 4U, 8U, 12U, 16U})); + EXPECT_EQ(blocks.kff.ColumnIndices(), + (std::vector{0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U, 0U, 1U, + 2U, 3U, 0U, 1U, 2U, 3U})); + EXPECT_EQ( + blocks.kff.Values(), + (std::vector{1.0, 3.0, 4.0, 6.0, 21.0, 23.0, 24.0, 26.0, 31.0, + 33.0, 34.0, 36.0, 51.0, 53.0, 54.0, 56.0})); + EXPECT_EQ(blocks.kfc.RowOffsets(), + (std::vector{0U, 2U, 4U, 6U, 8U})); + EXPECT_EQ(blocks.kfc.ColumnIndices(), + (std::vector{0U, 1U, 0U, 1U, 0U, 1U, 0U, 1U})); + EXPECT_EQ(blocks.kfc.Values(), + (std::vector{2.0, 5.0, 22.0, 0.0, 32.0, 35.0, 52.0, 55.0})); + EXPECT_EQ(blocks.kcf.RowOffsets(), (std::vector{0U, 4U, 8U})); + EXPECT_EQ(blocks.kcf.ColumnIndices(), + (std::vector{0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U})); + EXPECT_EQ(blocks.kcf.Values(), (std::vector{11.0, 13.0, 14.0, 16.0, + 41.0, 43.0, 44.0, 46.0})); + EXPECT_EQ(blocks.kcc.RowOffsets(), (std::vector{0U, 2U, 4U})); + EXPECT_EQ(blocks.kcc.ColumnIndices(), + (std::vector{0U, 1U, 0U, 1U})); + EXPECT_EQ(blocks.kcc.Values(), (std::vector{12.0, 15.0, 42.0, 45.0})); - EXPECT_EQ(blocks.kfc.Values()[3U], 0.0); - EXPECT_TRUE(blocks.kff.Validate().IsOk()); - EXPECT_TRUE(blocks.kfc.Validate().IsOk()); - EXPECT_TRUE(blocks.kcf.Validate().IsOk()); - EXPECT_TRUE(blocks.kcc.Validate().IsOk()); + EXPECT_EQ(blocks.kfc.Values()[3U], 0.0); + EXPECT_TRUE(blocks.kff.Validate().IsOk()); + EXPECT_TRUE(blocks.kfc.Validate().IsOk()); + EXPECT_TRUE(blocks.kcf.Validate().IsOk()); + EXPECT_TRUE(blocks.kcc.Validate().IsOk()); } TEST(EssentialConstraints, HandlesNoAllAndMixedConstraints) { - const auto full = makeMatrix(6U, 6U, sequentialDense(6U)); + const auto full = MakeMatrix(6U, 6U, SequentialDense(6U)); - const auto noConstraints = makeDofs({}); - auto none = fesa::EssentialConstraints::partition(full, noConstraints); - ASSERT_TRUE(none.HasValue()); - expectShape(none.Value().kff, 6U, 6U); - expectShape(none.Value().kfc, 6U, 0U); - expectShape(none.Value().kcf, 0U, 6U); - expectShape(none.Value().kcc, 0U, 0U); - EXPECT_EQ(none.Value().kff.Values(), full.Values()); + const auto no_constraints = MakeDofs({}); + auto none = fesa::EssentialConstraints::Partition(full, no_constraints); + ASSERT_TRUE(none.HasValue()); + ExpectShape(none.Value().kff, 6U, 6U); + ExpectShape(none.Value().kfc, 6U, 0U); + ExpectShape(none.Value().kcf, 0U, 6U); + ExpectShape(none.Value().kcc, 0U, 0U); + EXPECT_EQ(none.Value().kff.Values(), full.Values()); - const auto allConstraints = makeDofs({{"1", 1, 6, 1.0, {{}, 12U}}}); - auto all = fesa::EssentialConstraints::partition(full, allConstraints); - ASSERT_TRUE(all.HasValue()); - expectShape(all.Value().kff, 0U, 0U); - expectShape(all.Value().kfc, 0U, 6U); - expectShape(all.Value().kcf, 6U, 0U); - expectShape(all.Value().kcc, 6U, 6U); - EXPECT_EQ(all.Value().kcc.Values(), full.Values()); + const auto all_constraints = MakeDofs({{"1", 1, 6, 1.0, {{}, 12U}}}); + auto all = fesa::EssentialConstraints::Partition(full, all_constraints); + ASSERT_TRUE(all.HasValue()); + ExpectShape(all.Value().kff, 0U, 0U); + ExpectShape(all.Value().kfc, 0U, 6U); + ExpectShape(all.Value().kcf, 6U, 0U); + ExpectShape(all.Value().kcc, 6U, 6U); + EXPECT_EQ(all.Value().kcc.Values(), full.Values()); - const auto mixedConstraints = makeDofs({{"1", 3, 4, 0.0, {{}, 12U}}}); - auto mixed = fesa::EssentialConstraints::partition(full, mixedConstraints); - ASSERT_TRUE(mixed.HasValue()); - expectShape(mixed.Value().kff, 4U, 4U); - expectShape(mixed.Value().kfc, 4U, 2U); - expectShape(mixed.Value().kcf, 2U, 4U); - expectShape(mixed.Value().kcc, 2U, 2U); + const auto mixed_constraints = MakeDofs({{"1", 3, 4, 0.0, {{}, 12U}}}); + auto mixed = fesa::EssentialConstraints::Partition(full, mixed_constraints); + ASSERT_TRUE(mixed.HasValue()); + ExpectShape(mixed.Value().kff, 4U, 4U); + ExpectShape(mixed.Value().kfc, 4U, 2U); + ExpectShape(mixed.Value().kcf, 2U, 4U); + ExpectShape(mixed.Value().kcc, 2U, 2U); } // MITC4-DOF-003 -TEST(EssentialConstraints, PreservesShellSizedNoMixedAndAllConstraintRoundTrips) { - const auto full = makeMatrix(24U, 24U, sequentialDense(24U)); +TEST(EssentialConstraints, + PreservesShellSizedNoMixedAndAllConstraintRoundTrips) { + const auto full = MakeMatrix(24U, 24U, SequentialDense(24U)); - const auto noConstraints = makeShellSizedDofs({}); - auto none = fesa::EssentialConstraints::partition(full, noConstraints); - ASSERT_TRUE(none.HasValue()); - expectShape(none.Value().kff, 24U, 24U); - expectShape(none.Value().kfc, 24U, 0U); - expectShape(none.Value().kcf, 0U, 24U); - expectShape(none.Value().kcc, 0U, 0U); + const auto no_constraints = MakeShellSizedDofs({}); + auto none = fesa::EssentialConstraints::Partition(full, no_constraints); + ASSERT_TRUE(none.HasValue()); + ExpectShape(none.Value().kff, 24U, 24U); + ExpectShape(none.Value().kfc, 24U, 0U); + ExpectShape(none.Value().kcf, 0U, 24U); + ExpectShape(none.Value().kcc, 0U, 0U); - const auto mixedConstraints = makeShellSizedDofs({ - {"1", 1, 6, 0.0, {{}, 12U}}, - {"4", 2, 2, 2.5, {{}, 13U}}}); - auto mixed = fesa::EssentialConstraints::partition(full, mixedConstraints); - ASSERT_TRUE(mixed.HasValue()); - expectShape(mixed.Value().kff, 17U, 17U); - expectShape(mixed.Value().kfc, 17U, 7U); - expectShape(mixed.Value().kcf, 7U, 17U); - expectShape(mixed.Value().kcc, 7U, 7U); + const auto mixed_constraints = MakeShellSizedDofs( + {{"1", 1, 6, 0.0, {{}, 12U}}, {"4", 2, 2, 2.5, {{}, 13U}}}); + auto mixed = fesa::EssentialConstraints::Partition(full, mixed_constraints); + ASSERT_TRUE(mixed.HasValue()); + ExpectShape(mixed.Value().kff, 17U, 17U); + ExpectShape(mixed.Value().kfc, 17U, 7U); + ExpectShape(mixed.Value().kcf, 7U, 17U); + ExpectShape(mixed.Value().kcc, 7U, 7U); - fesa::Vector mixedFull{24U}; - for (std::size_t index = 0U; index < mixedFull.Size(); ++index) { - mixedFull[index] = static_cast(index) + 0.5; - } - for (std::size_t index = 0U; - index < mixedConstraints.constrainedDofCount(); - ++index) { - mixedFull[mixedConstraints.constrainedDofs()[index]] = - mixedConstraints.prescribedValues()[index]; - } - const auto mixedFree = - fesa::EssentialConstraints::gatherFree(mixedFull, mixedConstraints); - const auto mixedReconstructed = - fesa::EssentialConstraints::reconstructFull( - mixedFree, mixedConstraints.prescribedValues(), mixedConstraints); - ASSERT_EQ(mixedReconstructed.Size(), mixedFull.Size()); - for (std::size_t index = 0U; index < mixedFull.Size(); ++index) { - EXPECT_DOUBLE_EQ(mixedReconstructed[index], mixedFull[index]); - } - - const auto allConstraints = makeShellSizedDofs({ - {"1", 1, 6, 1.0, {{}, 14U}}, - {"2", 1, 6, 2.0, {{}, 15U}}, - {"3", 1, 6, 3.0, {{}, 16U}}, - {"4", 1, 6, 4.0, {{}, 17U}}}); - auto all = fesa::EssentialConstraints::partition(full, allConstraints); - ASSERT_TRUE(all.HasValue()); - expectShape(all.Value().kff, 0U, 0U); - expectShape(all.Value().kfc, 0U, 24U); - expectShape(all.Value().kcf, 24U, 0U); - expectShape(all.Value().kcc, 24U, 24U); - - const auto allReconstructed = - fesa::EssentialConstraints::reconstructFull( - fesa::Vector{0U}, - allConstraints.prescribedValues(), - allConstraints); - ASSERT_EQ(allReconstructed.Size(), 24U); - for (std::size_t node = 0U; node < 4U; ++node) { - for (std::size_t component = 0U; component < 6U; ++component) { - EXPECT_DOUBLE_EQ(allReconstructed[node * 6U + component], node + 1.0); - } + fesa::Vector mixed_full{24U}; + for (std::size_t index = 0U; index < mixed_full.Size(); ++index) { + mixed_full[index] = static_cast(index) + 0.5; + } + for (std::size_t index = 0U; index < mixed_constraints.ConstrainedDofCount(); + ++index) { + mixed_full[mixed_constraints.ConstrainedDofs()[index]] = + mixed_constraints.PrescribedValues()[index]; + } + const auto mixed_free = + fesa::EssentialConstraints::GatherFree(mixed_full, mixed_constraints); + const auto mixed_reconstructed = fesa::EssentialConstraints::ReconstructFull( + mixed_free, mixed_constraints.PrescribedValues(), mixed_constraints); + ASSERT_EQ(mixed_reconstructed.Size(), mixed_full.Size()); + for (std::size_t index = 0U; index < mixed_full.Size(); ++index) { + EXPECT_DOUBLE_EQ(mixed_reconstructed[index], mixed_full[index]); + } + + const auto all_constraints = + MakeShellSizedDofs({{"1", 1, 6, 1.0, {{}, 14U}}, + {"2", 1, 6, 2.0, {{}, 15U}}, + {"3", 1, 6, 3.0, {{}, 16U}}, + {"4", 1, 6, 4.0, {{}, 17U}}}); + auto all = fesa::EssentialConstraints::Partition(full, all_constraints); + ASSERT_TRUE(all.HasValue()); + ExpectShape(all.Value().kff, 0U, 0U); + ExpectShape(all.Value().kfc, 0U, 24U); + ExpectShape(all.Value().kcf, 24U, 0U); + ExpectShape(all.Value().kcc, 24U, 24U); + + const auto all_reconstructed = fesa::EssentialConstraints::ReconstructFull( + fesa::Vector{0U}, all_constraints.PrescribedValues(), all_constraints); + ASSERT_EQ(all_reconstructed.Size(), 24U); + for (std::size_t node = 0U; node < 4U; ++node) { + for (std::size_t component = 0U; component < 6U; ++component) { + EXPECT_DOUBLE_EQ(all_reconstructed[node * 6U + component], node + 1.0); } + } } TEST(EssentialConstraints, ReconstructsNonzeroPrescribedValues) { - const auto dofs = makeDofs({ - {"1", 2, 2, 2.5, {{}, 12U}}, - {"1", 5, 5, -3.25, {{}, 13U}}}); - fesa::Vector full{6U}; - full[0U] = 10.0; - full[1U] = 2.5; - full[2U] = 20.0; - full[3U] = 30.0; - full[4U] = -3.25; - full[5U] = 40.0; + const auto dofs = + MakeDofs({{"1", 2, 2, 2.5, {{}, 12U}}, {"1", 5, 5, -3.25, {{}, 13U}}}); + fesa::Vector full{6U}; + full[0U] = 10.0; + full[1U] = 2.5; + full[2U] = 20.0; + full[3U] = 30.0; + full[4U] = -3.25; + full[5U] = 40.0; - const auto free = fesa::EssentialConstraints::gatherFree(full, dofs); - const auto constrained = - fesa::EssentialConstraints::gatherConstrained(full, dofs); - EXPECT_EQ(free.Size(), 4U); - EXPECT_DOUBLE_EQ(free[0U], 10.0); - EXPECT_DOUBLE_EQ(free[1U], 20.0); - EXPECT_DOUBLE_EQ(free[2U], 30.0); - EXPECT_DOUBLE_EQ(free[3U], 40.0); - EXPECT_EQ(constrained.Size(), 2U); - EXPECT_DOUBLE_EQ(constrained[0U], 2.5); - EXPECT_DOUBLE_EQ(constrained[1U], -3.25); - EXPECT_EQ(constrained[0U], dofs.prescribedValues()[0U]); - EXPECT_EQ(constrained[1U], dofs.prescribedValues()[1U]); + const auto free = fesa::EssentialConstraints::GatherFree(full, dofs); + const auto constrained = + fesa::EssentialConstraints::GatherConstrained(full, dofs); + EXPECT_EQ(free.Size(), 4U); + EXPECT_DOUBLE_EQ(free[0U], 10.0); + EXPECT_DOUBLE_EQ(free[1U], 20.0); + EXPECT_DOUBLE_EQ(free[2U], 30.0); + EXPECT_DOUBLE_EQ(free[3U], 40.0); + EXPECT_EQ(constrained.Size(), 2U); + EXPECT_DOUBLE_EQ(constrained[0U], 2.5); + EXPECT_DOUBLE_EQ(constrained[1U], -3.25); + EXPECT_EQ(constrained[0U], dofs.PrescribedValues()[0U]); + EXPECT_EQ(constrained[1U], dofs.PrescribedValues()[1U]); - const auto reconstructed = fesa::EssentialConstraints::reconstructFull( - free, dofs.prescribedValues(), dofs); - ASSERT_EQ(reconstructed.Size(), full.Size()); - for (std::size_t index = 0U; index < full.Size(); ++index) { - EXPECT_DOUBLE_EQ(reconstructed[index], full[index]); - } + const auto reconstructed = fesa::EssentialConstraints::ReconstructFull( + free, dofs.PrescribedValues(), dofs); + ASSERT_EQ(reconstructed.Size(), full.Size()); + for (std::size_t index = 0U; index < full.Size(); ++index) { + EXPECT_DOUBLE_EQ(reconstructed[index], full[index]); + } } TEST(EssentialConstraints, RejectsDimensionOrOrderMismatch) { - const auto dofs = makeDofs({{"1", 2, 2, 1.0, {{}, 12U}}}); - const auto wrongSquare = makeMatrix(5U, 5U, sequentialDense(5U)); - auto wrongDimension = - fesa::EssentialConstraints::partition(wrongSquare, dofs); - ASSERT_FALSE(wrongDimension.HasValue()); - EXPECT_EQ( - wrongDimension.GetStatus().Category(), - fesa::FailureCategory::kModel); - ASSERT_EQ(wrongDimension.GetStatus().Diagnostics().size(), 1U); - EXPECT_EQ( - wrongDimension.GetStatus().Diagnostics()[0U].code, - "invalid-constraint-dimensions"); + const auto dofs = MakeDofs({{"1", 2, 2, 1.0, {{}, 12U}}}); + const auto wrong_square = MakeMatrix(5U, 5U, SequentialDense(5U)); + auto wrong_dimension = + fesa::EssentialConstraints::Partition(wrong_square, dofs); + ASSERT_FALSE(wrong_dimension.HasValue()); + EXPECT_EQ(wrong_dimension.GetStatus().Category(), + fesa::FailureCategory::kModel); + ASSERT_EQ(wrong_dimension.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(wrong_dimension.GetStatus().Diagnostics()[0U].code, + "invalid-constraint-dimensions"); - const auto rectangular = makeMatrix( - 6U, 5U, std::vector(30U, 0.0)); - auto wrongOrder = fesa::EssentialConstraints::partition(rectangular, dofs); - ASSERT_FALSE(wrongOrder.HasValue()); - EXPECT_EQ( - wrongOrder.GetStatus().Diagnostics()[0U].code, - "invalid-constraint-dimensions"); + const auto rectangular = MakeMatrix(6U, 5U, std::vector(30U, 0.0)); + auto wrong_order = fesa::EssentialConstraints::Partition(rectangular, dofs); + ASSERT_FALSE(wrong_order.HasValue()); + EXPECT_EQ(wrong_order.GetStatus().Diagnostics()[0U].code, + "invalid-constraint-dimensions"); - EXPECT_THROW( - static_cast(fesa::EssentialConstraints::gatherFree( - fesa::Vector{5U}, dofs)), - std::invalid_argument); - EXPECT_THROW( - static_cast(fesa::EssentialConstraints::gatherConstrained( - fesa::Vector{7U}, dofs)), - std::invalid_argument); - EXPECT_THROW( - static_cast(fesa::EssentialConstraints::reconstructFull( - fesa::Vector{4U}, fesa::Vector{2U}, dofs)), - std::invalid_argument); + EXPECT_THROW(static_cast(fesa::EssentialConstraints::GatherFree( + fesa::Vector{5U}, dofs)), + std::invalid_argument); + EXPECT_THROW(static_cast(fesa::EssentialConstraints::GatherConstrained( + fesa::Vector{7U}, dofs)), + std::invalid_argument); + EXPECT_THROW(static_cast(fesa::EssentialConstraints::ReconstructFull( + fesa::Vector{4U}, fesa::Vector{2U}, dofs)), + std::invalid_argument); } diff --git a/tests/unit/fem/dof_manager_test.cpp b/tests/unit/fem/dof_manager_test.cpp index c1aca0d..af54af2 100644 --- a/tests/unit/fem/dof_manager_test.cpp +++ b/tests/unit/fem/dof_manager_test.cpp @@ -1,4 +1,4 @@ -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include @@ -11,243 +11,227 @@ namespace { -fesa::ModelDefinition makeDefinition() { - const std::filesystem::path source{"models/dof-manager.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{"Beam-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Beam-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, - {{"Beam-1", 30, "30"}, {2.0, 0.0, 0.0}, {source, 12U}}}; - definition.materials = { - {"Material", 1000.0, 0.25, {source, 20U}}}; - definition.sections = {{ - "Section", 1.0, 1.0, 0.0, 1.0, 1.0, - {0.0, 1.0, 0.0}, {}, {source, 30U}}}; - definition.elements = { - {{"Beam-1", 100, "100"}, {0U, 1U}, 0U, 0U, {source, 40U}}, - {{"Beam-1", 200, "200"}, {1U, 2U}, 0U, 0U, {source, 41U}}}; - definition.node_sets = { - {"Root", std::optional{"Beam-1"}, {0U}, {source, 50U}}, - {"Ends", std::optional{"Beam-1"}, {0U, 2U}, {source, 51U}}}; - definition.steps = {{ - "Step-1", - {{"Root", 1, 2, 0.0, {source, 60U}}, - {"ends", 3, 3, 0.25, {source, 61U}}, - {"20", 6, 6, -0.5, {source, 62U}}, - {"Root", 1, 1, 0.0, {source, 63U}}}, - {}, - 0.1, - 1.0, - 0.01, - 1.0, - {source, 59U}}}; - return definition; +fesa::ModelDefinition MakeDefinition() { + const std::filesystem::path source{"models/dof-manager.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = {{{"Beam-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Beam-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, + {{"Beam-1", 30, "30"}, {2.0, 0.0, 0.0}, {source, 12U}}}; + definition.materials = {{"Material", 1000.0, 0.25, {source, 20U}}}; + definition.sections = { + {"Section", 1.0, 1.0, 0.0, 1.0, 1.0, {0.0, 1.0, 0.0}, {}, {source, 30U}}}; + definition.elements = { + {{"Beam-1", 100, "100"}, {0U, 1U}, 0U, 0U, {source, 40U}}, + {{"Beam-1", 200, "200"}, {1U, 2U}, 0U, 0U, {source, 41U}}}; + definition.node_sets = { + {"Root", std::optional{"Beam-1"}, {0U}, {source, 50U}}, + {"Ends", std::optional{"Beam-1"}, {0U, 2U}, {source, 51U}}}; + definition.steps = {{"Step-1", + {{"Root", 1, 2, 0.0, {source, 60U}}, + {"ends", 3, 3, 0.25, {source, 61U}}, + {"20", 6, 6, -0.5, {source, 62U}}, + {"Root", 1, 1, 0.0, {source, 63U}}}, + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 59U}}}; + return definition; } -fesa::ModelDefinition makeShellDefinition() { - const std::filesystem::path source{"models/shell-dof-manager.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:1122334455667788"; - definition.nodes = { - {{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, - {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 12U}}, - {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 13U}}}; - definition.materials = { - {"Material", 1000.0, 0.25, {source, 20U}}}; - definition.shell_sections = { - {"ShellSection", 0.1, 0U, {source, 30U}}}; - definition.shell_elements = {{ - {"Shell-1", 100, "100"}, - fesa::ShellSourceElementType::kS4, - {2U, 0U, 3U, 1U}, - 0U, - 0U, - {source, 40U}}}; - definition.steps = {{ - "Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; - return definition; +fesa::ModelDefinition MakeShellDefinition() { + const std::filesystem::path source{"models/shell-dof-manager.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:1122334455667788"; + definition.nodes = {{{"Shell-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Shell-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}}, + {{"Shell-1", 30, "30"}, {1.0, 1.0, 0.0}, {source, 12U}}, + {{"Shell-1", 40, "40"}, {0.0, 1.0, 0.0}, {source, 13U}}}; + definition.materials = {{"Material", 1000.0, 0.25, {source, 20U}}}; + definition.shell_sections = {{"ShellSection", 0.1, 0U, {source, 30U}}}; + definition.shell_elements = {{{"Shell-1", 100, "100"}, + fesa::ShellSourceElementType::kS4, + {2U, 0U, 3U, 1U}, + 0U, + 0U, + {source, 40U}}}; + definition.steps = {{"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {source, 50U}}}; + return definition; } struct DofFixture { - fesa::DofManager dofs; + fesa::DofManager dofs; }; -DofFixture makeDofFixture(fesa::ModelDefinition definition = makeDefinition()) { - auto domain = fesa::Domain::Create(std::move(definition)); - EXPECT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - EXPECT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - EXPECT_TRUE(dofs.HasValue()); - return {std::move(dofs.Value())}; +DofFixture MakeDofFixture(fesa::ModelDefinition definition = MakeDefinition()) { + auto domain = fesa::Domain::Create(std::move(definition)); + EXPECT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + EXPECT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + EXPECT_TRUE(dofs.HasValue()); + return {std::move(dofs.Value())}; } -std::vector rowColumns( - const fesa::SparsePattern& pattern, std::size_t row) { - return { - pattern.columnIndices.begin() + pattern.rowOffsets[row], - pattern.columnIndices.begin() + pattern.rowOffsets[row + 1U]}; +std::vector RowColumns(const fesa::SparsePattern& pattern, + std::size_t row) { + return {pattern.column_indices.begin() + pattern.row_offsets[row], + pattern.column_indices.begin() + pattern.row_offsets[row + 1U]}; } -} // namespace +} // namespace TEST(DofManager, NumbersSixDofsAndFreeEquationsStably) { - const auto fixture = makeDofFixture(); - const auto& dofs = fixture.dofs; + const auto fixture = MakeDofFixture(); + const auto& dofs = fixture.dofs; - EXPECT_EQ(dofs.fullDofCount(), 18U); - EXPECT_EQ(dofs.freeDofCount(), 13U); - EXPECT_EQ(dofs.constrainedDofCount(), 5U); - EXPECT_EQ(dofs.fullDof(0U, fesa::DofComponent::ux), 0U); - EXPECT_EQ(dofs.fullDof(0U, fesa::DofComponent::urz), 5U); - EXPECT_EQ(dofs.fullDof(1U, fesa::DofComponent::ux), 6U); - EXPECT_EQ(dofs.fullDof(2U, fesa::DofComponent::urz), 17U); - EXPECT_EQ( - dofs.freeDofs(), - (std::vector{ - 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 12U, 13U, 15U, 16U, 17U})); - EXPECT_EQ( - dofs.constrainedDofs(), - (std::vector{0U, 1U, 2U, 11U, 14U})); - EXPECT_EQ(dofs.freeEquation(0U), std::nullopt); - EXPECT_EQ(dofs.freeEquation(3U), std::optional{0U}); - EXPECT_EQ(dofs.freeEquation(10U), std::optional{7U}); - EXPECT_EQ(dofs.freeEquation(17U), std::optional{12U}); + EXPECT_EQ(dofs.FullDofCount(), 18U); + EXPECT_EQ(dofs.FreeDofCount(), 13U); + EXPECT_EQ(dofs.ConstrainedDofCount(), 5U); + EXPECT_EQ(dofs.FullDof(0U, fesa::DofComponent::kUx), 0U); + EXPECT_EQ(dofs.FullDof(0U, fesa::DofComponent::kUrz), 5U); + EXPECT_EQ(dofs.FullDof(1U, fesa::DofComponent::kUx), 6U); + EXPECT_EQ(dofs.FullDof(2U, fesa::DofComponent::kUrz), 17U); + EXPECT_EQ(dofs.FreeDofs(), + (std::vector{3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 12U, 13U, + 15U, 16U, 17U})); + EXPECT_EQ(dofs.ConstrainedDofs(), + (std::vector{0U, 1U, 2U, 11U, 14U})); + EXPECT_EQ(dofs.FreeEquation(0U), std::nullopt); + EXPECT_EQ(dofs.FreeEquation(3U), std::optional{0U}); + EXPECT_EQ(dofs.FreeEquation(10U), std::optional{7U}); + EXPECT_EQ(dofs.FreeEquation(17U), std::optional{12U}); } TEST(DofManager, ExpandsAndValidatesPrescribedValues) { - const auto fixture = makeDofFixture(); - const auto& values = fixture.dofs.prescribedValues(); - ASSERT_EQ(values.Size(), 5U); - EXPECT_DOUBLE_EQ(values[0], 0.0); - EXPECT_DOUBLE_EQ(values[1], 0.0); - EXPECT_DOUBLE_EQ(values[2], 0.25); - EXPECT_DOUBLE_EQ(values[3], -0.5); - EXPECT_DOUBLE_EQ(values[4], 0.25); + const auto fixture = MakeDofFixture(); + const auto& values = fixture.dofs.PrescribedValues(); + ASSERT_EQ(values.Size(), 5U); + EXPECT_DOUBLE_EQ(values[0], 0.0); + EXPECT_DOUBLE_EQ(values[1], 0.0); + EXPECT_DOUBLE_EQ(values[2], 0.25); + EXPECT_DOUBLE_EQ(values[3], -0.5); + EXPECT_DOUBLE_EQ(values[4], 0.25); - auto conflictingDefinition = makeDefinition(); - conflictingDefinition.steps[0].boundaries.push_back( - {"root", 1, 1, 1.0, {conflictingDefinition.source_path, 77U}}); - auto domain = fesa::Domain::Create(std::move(conflictingDefinition)); - ASSERT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - ASSERT_TRUE(model.HasValue()); + auto conflicting_definition = MakeDefinition(); + conflicting_definition.steps[0].boundaries.push_back( + {"root", 1, 1, 1.0, {conflicting_definition.source_path, 77U}}); + auto domain = fesa::Domain::Create(std::move(conflicting_definition)); + ASSERT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + ASSERT_TRUE(model.HasValue()); - auto conflict = fesa::DofManager::create(model.Value()); - ASSERT_FALSE(conflict.HasValue()); - EXPECT_EQ(conflict.GetStatus().Category(), fesa::FailureCategory::kInput); - ASSERT_EQ(conflict.GetStatus().Diagnostics().size(), 1U); - const auto& diagnostic = conflict.GetStatus().Diagnostics()[0]; - EXPECT_EQ(diagnostic.code, "conflicting-boundary-condition"); - EXPECT_EQ(diagnostic.keyword, "BOUNDARY"); - EXPECT_EQ(diagnostic.entity_identity, "root"); - EXPECT_EQ(diagnostic.location.file, std::filesystem::path{"models/dof-manager.inp"}); - EXPECT_EQ(diagnostic.location.line, 77U); + auto conflict = fesa::DofManager::Create(model.Value()); + ASSERT_FALSE(conflict.HasValue()); + EXPECT_EQ(conflict.GetStatus().Category(), fesa::FailureCategory::kInput); + ASSERT_EQ(conflict.GetStatus().Diagnostics().size(), 1U); + const auto& diagnostic = conflict.GetStatus().Diagnostics()[0]; + EXPECT_EQ(diagnostic.code, "conflicting-boundary-condition"); + EXPECT_EQ(diagnostic.keyword, "BOUNDARY"); + EXPECT_EQ(diagnostic.entity_identity, "root"); + EXPECT_EQ(diagnostic.location.file, + std::filesystem::path{"models/dof-manager.inp"}); + EXPECT_EQ(diagnostic.location.line, 77U); } TEST(DofManager, BuildsTwelveDofScatterAndSortedUniquePattern) { - const auto fixture = makeDofFixture(); - const auto& dofs = fixture.dofs; + const auto fixture = MakeDofFixture(); + const auto& dofs = fixture.dofs; - EXPECT_EQ( - dofs.elementScatter(0U), - (std::array{ - 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U})); - EXPECT_EQ( - dofs.elementScatter(1U), - (std::array{ - 6U, 7U, 8U, 9U, 10U, 11U, - 12U, 13U, 14U, 15U, 16U, 17U})); + EXPECT_EQ(dofs.ElementScatter(0U), + (std::array{0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, + 10U, 11U})); + EXPECT_EQ(dofs.ElementScatter(1U), + (std::array{6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, + 14U, 15U, 16U, 17U})); - const auto& pattern = dofs.sparsePattern(); - EXPECT_EQ( - pattern.rowOffsets, - (std::vector{ - 0U, 12U, 24U, 36U, 48U, 60U, 72U, - 90U, 108U, 126U, 144U, 162U, 180U, - 192U, 204U, 216U, 228U, 240U, 252U})); - const std::vector firstBlock{ - 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U}; - const std::vector sharedBlock{ - 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, - 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; - const std::vector lastBlock{ - 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; - EXPECT_EQ(rowColumns(pattern, 0U), firstBlock); - EXPECT_EQ(rowColumns(pattern, 7U), sharedBlock); - EXPECT_EQ(rowColumns(pattern, 17U), lastBlock); - for (std::size_t row = 0U; row < dofs.fullDofCount(); ++row) { - const auto columns = rowColumns(pattern, row); - EXPECT_TRUE(std::is_sorted(columns.begin(), columns.end())); - EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end()); - } + const auto& pattern = dofs.GetSparsePattern(); + EXPECT_EQ(pattern.row_offsets, + (std::vector{0U, 12U, 24U, 36U, 48U, 60U, 72U, 90U, + 108U, 126U, 144U, 162U, 180U, 192U, 204U, + 216U, 228U, 240U, 252U})); + const std::vector first_block{0U, 1U, 2U, 3U, 4U, 5U, + 6U, 7U, 8U, 9U, 10U, 11U}; + const std::vector shared_block{0U, 1U, 2U, 3U, 4U, 5U, + 6U, 7U, 8U, 9U, 10U, 11U, + 12U, 13U, 14U, 15U, 16U, 17U}; + const std::vector last_block{6U, 7U, 8U, 9U, 10U, 11U, + 12U, 13U, 14U, 15U, 16U, 17U}; + EXPECT_EQ(RowColumns(pattern, 0U), first_block); + EXPECT_EQ(RowColumns(pattern, 7U), shared_block); + EXPECT_EQ(RowColumns(pattern, 17U), last_block); + for (std::size_t row = 0U; row < dofs.FullDofCount(); ++row) { + const auto columns = RowColumns(pattern, row); + EXPECT_TRUE(std::is_sorted(columns.begin(), columns.end())); + EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), + columns.end()); + } } // MITC4-DOF-001 TEST(DofManager, BuildsShellScatterInSourceNodeAndComponentOrder) { - const auto fixture = makeDofFixture(makeShellDefinition()); - const auto& dofs = fixture.dofs; + const auto fixture = MakeDofFixture(MakeShellDefinition()); + const auto& dofs = fixture.dofs; - EXPECT_EQ(dofs.fullDofCount(), 24U); - EXPECT_EQ( - dofs.shellElementScatter(0U), - (std::array{ - 12U, 13U, 14U, 15U, 16U, 17U, - 0U, 1U, 2U, 3U, 4U, 5U, - 18U, 19U, 20U, 21U, 22U, 23U, - 6U, 7U, 8U, 9U, 10U, 11U})); + EXPECT_EQ(dofs.FullDofCount(), 24U); + EXPECT_EQ(dofs.ShellElementScatter(0U), + (std::array{ + 12U, 13U, 14U, 15U, 16U, 17U, 0U, 1U, 2U, 3U, 4U, 5U, + 18U, 19U, 20U, 21U, 22U, 23U, 6U, 7U, 8U, 9U, 10U, 11U})); } // MITC4-DOF-002 TEST(DofManager, IncludesShellConnectivityInSortedUniqueSparsePattern) { - const auto fixture = makeDofFixture(makeShellDefinition()); - const auto& dofs = fixture.dofs; - const auto& pattern = dofs.sparsePattern(); + const auto fixture = MakeDofFixture(MakeShellDefinition()); + const auto& dofs = fixture.dofs; + const auto& pattern = dofs.GetSparsePattern(); - ASSERT_EQ(pattern.rowOffsets.size(), 25U); - ASSERT_EQ(pattern.columnIndices.size(), 24U * 24U); - for (std::size_t row = 0U; row < dofs.fullDofCount(); ++row) { - EXPECT_EQ(pattern.rowOffsets[row], row * 24U); - EXPECT_EQ(pattern.rowOffsets[row + 1U], (row + 1U) * 24U); - const auto columns = rowColumns(pattern, row); - ASSERT_EQ(columns.size(), 24U); - for (std::size_t column = 0U; column < columns.size(); ++column) { - EXPECT_EQ(columns[column], column); - } - EXPECT_TRUE(std::binary_search(columns.begin(), columns.end(), row)); - EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end()); + ASSERT_EQ(pattern.row_offsets.size(), 25U); + ASSERT_EQ(pattern.column_indices.size(), 24U * 24U); + for (std::size_t row = 0U; row < dofs.FullDofCount(); ++row) { + EXPECT_EQ(pattern.row_offsets[row], row * 24U); + EXPECT_EQ(pattern.row_offsets[row + 1U], (row + 1U) * 24U); + const auto columns = RowColumns(pattern, row); + ASSERT_EQ(columns.size(), 24U); + for (std::size_t column = 0U; column < columns.size(); ++column) { + EXPECT_EQ(columns[column], column); } + EXPECT_TRUE(std::binary_search(columns.begin(), columns.end(), row)); + EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), + columns.end()); + } } TEST(DofManager, ReconstructsFullReducedRoundTrip) { - const auto fixture = makeDofFixture(); - const auto& dofs = fixture.dofs; - fesa::Vector full{dofs.fullDofCount()}; - for (std::size_t index = 0U; index < full.Size(); ++index) { - full[index] = static_cast(index) + 0.5; - } - for (std::size_t index = 0U; index < dofs.constrainedDofCount(); ++index) { - full[dofs.constrainedDofs()[index]] = dofs.prescribedValues()[index]; - } + const auto fixture = MakeDofFixture(); + const auto& dofs = fixture.dofs; + fesa::Vector full{dofs.FullDofCount()}; + for (std::size_t index = 0U; index < full.Size(); ++index) { + full[index] = static_cast(index) + 0.5; + } + for (std::size_t index = 0U; index < dofs.ConstrainedDofCount(); ++index) { + full[dofs.ConstrainedDofs()[index]] = dofs.PrescribedValues()[index]; + } - fesa::Vector reduced{dofs.freeDofCount()}; - for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { - reduced[equation] = full[dofs.freeDofs()[equation]]; - } - fesa::Vector reconstructed{dofs.fullDofCount()}; - for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { - reconstructed[dofs.freeDofs()[equation]] = reduced[equation]; - } - for (std::size_t index = 0U; index < dofs.constrainedDofCount(); ++index) { - reconstructed[dofs.constrainedDofs()[index]] = dofs.prescribedValues()[index]; - } + fesa::Vector reduced{dofs.FreeDofCount()}; + for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { + reduced[equation] = full[dofs.FreeDofs()[equation]]; + } + fesa::Vector reconstructed{dofs.FullDofCount()}; + for (std::size_t equation = 0U; equation < reduced.Size(); ++equation) { + reconstructed[dofs.FreeDofs()[equation]] = reduced[equation]; + } + for (std::size_t index = 0U; index < dofs.ConstrainedDofCount(); ++index) { + reconstructed[dofs.ConstrainedDofs()[index]] = + dofs.PrescribedValues()[index]; + } - ASSERT_EQ(reconstructed.Size(), full.Size()); - for (std::size_t index = 0U; index < full.Size(); ++index) { - EXPECT_DOUBLE_EQ(reconstructed[index], full[index]); - } + ASSERT_EQ(reconstructed.Size(), full.Size()); + for (std::size_t index = 0U; index < full.Size(); ++index) { + EXPECT_DOUBLE_EQ(reconstructed[index], full[index]); + } } diff --git a/tests/unit/io/hdf5/hdf5_results_writer_test.cpp b/tests/unit/io/hdf5/hdf5_results_writer_test.cpp index 78cc04a..f3ed388 100644 --- a/tests/unit/io/hdf5/hdf5_results_writer_test.cpp +++ b/tests/unit/io/hdf5/hdf5_results_writer_test.cpp @@ -3,10 +3,10 @@ #include "fesa/io/hdf5/hdf5_results_writer.hpp" -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/analysis/analysis_state.hpp" +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" #include "fesa/build_info.h" -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include "fesa/model/domain.h" #include @@ -164,30 +164,30 @@ WriterFixture makeFixture( auto domain = std::make_unique( std::move(domainResult.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); + auto modelResult = fesa::AnalysisModel::Create(*domain); if (!modelResult.HasValue()) { throw std::runtime_error{"Writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::create(model); + auto dofsResult = fesa::DofManager::Create(model); if (!dofsResult.HasValue()) { throw std::runtime_error{"Writer fixture DofManager construction failed."}; } auto dofs = std::make_unique( std::move(dofsResult.Value())); auto state = std::make_unique( - fesa::AnalysisState::create(*dofs, {"Step-1", 0U})); + fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); - for (std::size_t index = 0U; index < state->displacement().Size(); ++index) { - state->displacement()[index] = 0.25 + static_cast(index); - state->externalForce()[index] = 100.0 + static_cast(index); - state->internalForce()[index] = 200.0 + 2.0 * static_cast(index); - state->residual()[index] = 100.0 + static_cast(index); - state->reaction()[index] = 100.0 + static_cast(index); + for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { + state->Displacement()[index] = 0.25 + static_cast(index); + state->ExternalForce()[index] = 100.0 + static_cast(index); + state->InternalForce()[index] = 200.0 + 2.0 * static_cast(index); + state->Residual()[index] = 100.0 + static_cast(index); + state->Reaction()[index] = 100.0 + static_cast(index); } const auto& nodes = domain->Nodes(); - state->endpointResults() = { + state->EndpointResults() = { {0U, 0, nodes[0U].source_id, @@ -198,15 +198,15 @@ WriterFixture makeFixture( nodes[1U].source_id, {7.0, 8.0, 9.0, 10.0, 11.0, 12.0}, {15.0, 16.0, 17.0, 18.0}}}; - state->gaussResults() = { + state->GaussResults() = { {0U, 1, {0.01, 0.02, 0.03, 0.04}, {21.0, 22.0, 23.0, 24.0}}, {0U, 2, {0.05, 0.06, 0.07, 0.08}, {25.0, 26.0, 27.0, 28.0}}}; if (useDefaultCentroid) { - state->stressResults() = { + state->StressResults() = { {0U, 1, 0U, 0.0, 0.0, 31.0, "fesa-default"}, {0U, 2, 0U, 0.0, 0.0, 32.0, "fesa-default"}}; } else { - state->stressResults() = { + state->StressResults() = { {0U, 1, 1U, -0.1, 0.2, 31.0, "input"}, {0U, 1, 2U, 0.3, -0.4, 32.0, "input"}, {0U, 2, 1U, -0.1, 0.2, 33.0, "input"}, @@ -264,25 +264,25 @@ WriterFixture makeShellFixture(const std::filesystem::path& source) { } auto domain = std::make_unique( std::move(domainResult.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); + auto modelResult = fesa::AnalysisModel::Create(*domain); if (!modelResult.HasValue()) { throw std::runtime_error{"Shell writer fixture AnalysisModel construction failed."}; } const fesa::AnalysisModel model = std::move(modelResult.Value()); - auto dofsResult = fesa::DofManager::create(model); + auto dofsResult = fesa::DofManager::Create(model); if (!dofsResult.HasValue()) { throw std::runtime_error{"Shell writer fixture DofManager construction failed."}; } auto dofs = std::make_unique( std::move(dofsResult.Value())); auto state = std::make_unique( - fesa::AnalysisState::create(*dofs, {"Step-1", 0U})); - for (std::size_t index = 0U; index < state->displacement().Size(); ++index) { - state->displacement()[index] = 0.01 * static_cast(index + 1U); - state->externalForce()[index] = 10.0 + static_cast(index); - state->internalForce()[index] = 20.0 + static_cast(index); - state->residual()[index] = 30.0 + static_cast(index); - state->reaction()[index] = 40.0 + static_cast(index); + fesa::AnalysisState::Create(*dofs, {"Step-1", 0U})); + for (std::size_t index = 0U; index < state->Displacement().Size(); ++index) { + state->Displacement()[index] = 0.01 * static_cast(index + 1U); + state->ExternalForce()[index] = 10.0 + static_cast(index); + state->InternalForce()[index] = 20.0 + static_cast(index); + state->Residual()[index] = 30.0 + static_cast(index); + state->Reaction()[index] = 40.0 + static_cast(index); } const double gauss = 1.0 / std::sqrt(3.0); @@ -292,10 +292,10 @@ WriterFixture makeShellFixture(const std::filesystem::path& source) { {gauss, gauss}, {-gauss, gauss}}}; const std::array locations{ - fesa::ShellMidsurfaceLocation::gp1, - fesa::ShellMidsurfaceLocation::gp2, - fesa::ShellMidsurfaceLocation::gp3, - fesa::ShellMidsurfaceLocation::gp4}; + fesa::ShellMidsurfaceLocation::kGp1, + fesa::ShellMidsurfaceLocation::kGp2, + fesa::ShellMidsurfaceLocation::kGp3, + fesa::ShellMidsurfaceLocation::kGp4}; fesa::ShellStateCandidate candidate{}; for (std::size_t point = 0U; point < locations.size(); ++point) { const double base = 100.0 * static_cast(point + 1U); @@ -310,20 +310,20 @@ WriterFixture makeShellFixture(const std::filesystem::path& source) { base + 5.0, base + 6.0, base + 7.0, base + 8.0}, {base + 11.0, base + 12.0, base + 13.0, base + 14.0, base + 15.0, base + 16.0, base + 17.0, base + 18.0}, - {{{fesa::ShellSectionPosition::bottom, + {{{fesa::ShellSectionPosition::kBottom, -1.0, {base + 21.0, base + 22.0, base + 23.0}}, - {fesa::ShellSectionPosition::middle, + {fesa::ShellSectionPosition::kMiddle, 0.0, {base + 24.0, base + 25.0, base + 26.0}}, - {fesa::ShellSectionPosition::top, + {fesa::ShellSectionPosition::kTop, 1.0, {base + 27.0, base + 28.0, base + 29.0}}}}}); } - candidate.physicalStrainEnergy = 123.5; + candidate.physical_strain_energy = 123.5; candidate.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; - candidate.verificationMetrics = {1.0e-13, 2.0e-13, 3.0e-13}; - const fesa::Status commit = state->commitShellResults( + candidate.verification_metrics = {1.0e-13, 2.0e-13, 3.0e-13}; + const fesa::Status commit = state->CommitShellResults( {0U}, std::move(candidate)); if (!commit.IsOk()) { throw std::runtime_error{"Shell writer fixture state commit failed."}; @@ -846,7 +846,7 @@ TEST(Hdf5ResultsWriter, WritesExactSchemaShapesAttributesAndIdentity) { const auto output = directory.path() / "results.h5"; fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.write(output, *fixture.domain, *fixture.state, {}).IsOk()); + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const auto file = openFile(output); @@ -1041,7 +1041,7 @@ TEST(Hdf5ResultsWriter, WritesMandatoryOutputsDespiteOutputRequests) { fesa::Hdf5ResultsWriter writer; ASSERT_TRUE( - writer.write(output, *fixture.domain, *fixture.state, {ignoredRequest}) + writer.Write(output, *fixture.domain, *fixture.state, {ignoredRequest}) .IsOk()); const auto file = openFile(output); for (const char* suffix : { @@ -1078,7 +1078,7 @@ TEST(Hdf5ResultsWriter, WritesWarningsAndDefaultCentroid) { const auto output = directory.path() / "results.h5"; fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.write(output, *fixture.domain, *fixture.state, diagnostics).IsOk()); + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, diagnostics).IsOk()); const auto file = openFile(output); auto stressRows = readStressRows(file.get()); ASSERT_EQ(stressRows.size(), 2U); @@ -1116,15 +1116,15 @@ TEST(Hdf5ResultsWriter, FailureLeavesNoPartialAndPreservesExistingFinal) { auto fixture = makeFixture(directory.path() / "failure.inp"); fesa::Hdf5ResultsWriter writer; - fixture.state->displacement()[0U] = + fixture.state->Displacement()[0U] = std::numeric_limits::quiet_NaN(); const auto invalidOutput = directory.path() / "invalid-results.h5"; expectOutputFailure( - writer.write(invalidOutput, *fixture.domain, *fixture.state, {}), + writer.Write(invalidOutput, *fixture.domain, *fixture.state, {}), "invalid-result-state"); EXPECT_FALSE(std::filesystem::exists(invalidOutput)); EXPECT_EQ(entryCount(directory.path()), 0U); - fixture.state->displacement()[0U] = 0.25; + fixture.state->Displacement()[0U] = 0.25; const auto final = directory.path() / "results.h5"; const std::vector sentinel = {'p', 'r', 'e', 'v', 'i', 'o', 'u', 's'}; @@ -1140,7 +1140,7 @@ TEST(Hdf5ResultsWriter, FailureLeavesNoPartialAndPreservesExistingFinal) { ASSERT_NE(lock.get(), INVALID_HANDLE_VALUE); expectOutputFailure( - writer.write(final, *fixture.domain, *fixture.state, {}), + writer.Write(final, *fixture.domain, *fixture.state, {}), "hdf5-finalization-failure"); EXPECT_EQ(readBytes(final), sentinel); EXPECT_EQ(entryCount(directory.path()), 1U); @@ -1153,7 +1153,7 @@ TEST(Hdf5ResultsWriter, SuccessfullyReplacesExistingFinal) { writeBytes(final, {'o', 'l', 'd'}); fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.write(final, *fixture.domain, *fixture.state, {}).IsOk()); + ASSERT_TRUE(writer.Write(final, *fixture.domain, *fixture.state, {}).IsOk()); EXPECT_GT(H5Fis_hdf5(final.string().c_str()), 0); EXPECT_EQ(entryCount(directory.path()), 1U); const auto file = openFile(final); @@ -1171,7 +1171,7 @@ TEST(Hdf5ResultsWriter, WritesExactShellMetadataAndModelIdentity) { const auto output = directory.path() / "results.h5"; fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.write(output, *fixture.domain, *fixture.state, {}).IsOk()); + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); const auto file = openFile(output); Hdf5Handle metadata{ @@ -1261,7 +1261,7 @@ TEST(Hdf5ResultsWriter, WritesExactMandatoryShellResultInventory) { const auto output = directory.path() / "results.h5"; fesa::Hdf5ResultsWriter writer; - ASSERT_TRUE(writer.write(output, *fixture.domain, *fixture.state, {}).IsOk()); + ASSERT_TRUE(writer.Write(output, *fixture.domain, *fixture.state, {}).IsOk()); const auto file = openFile(output); const std::string shellRoot = std::string{kStepRoot} + "/element/shell"; expectNumericDataset( @@ -1329,7 +1329,7 @@ TEST(Hdf5ResultsWriter, WritesShellInventoryDespiteRequestsAndOmitsForbiddenPath fesa::Hdf5ResultsWriter writer; ASSERT_TRUE( - writer.write(output, *fixture.domain, *fixture.state, {ignoredRequest}) + writer.Write(output, *fixture.domain, *fixture.state, {ignoredRequest}) .IsOk()); const auto file = openFile(output); for (const char* suffix : { @@ -1359,7 +1359,7 @@ TEST(Hdf5ResultsWriter, WritesShellInventoryDespiteRequestsAndOmitsForbiddenPath TEST(Hdf5ResultsWriter, InvalidShellInventoryPreservesExistingFinal) { TempDirectory directory{"shell-atomic"}; auto fixture = makeShellFixture(directory.path() / "shell.inp"); - auto invalidState = fesa::AnalysisState::create( + auto invalidState = fesa::AnalysisState::Create( *fixture.dofs, {"Step-1", 0U}); const auto final = directory.path() / "results.h5"; const std::vector sentinel = {'s', 'h', 'e', 'l', 'l'}; @@ -1367,7 +1367,7 @@ TEST(Hdf5ResultsWriter, InvalidShellInventoryPreservesExistingFinal) { fesa::Hdf5ResultsWriter writer; expectOutputFailure( - writer.write(final, *fixture.domain, invalidState, {}), + writer.Write(final, *fixture.domain, invalidState, {}), "invalid-result-rows"); EXPECT_EQ(readBytes(final), sentinel); EXPECT_EQ(entryCount(directory.path()), 1U); diff --git a/tests/unit/math/sparse_matrix_test.cpp b/tests/unit/math/sparse_matrix_test.cpp index 84d201c..7754a2f 100644 --- a/tests/unit/math/sparse_matrix_test.cpp +++ b/tests/unit/math/sparse_matrix_test.cpp @@ -8,7 +8,7 @@ #include #include -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include "fesa/math/matrix.h" namespace { @@ -37,8 +37,8 @@ TEST(SparseAssembly, ValidatesKnownCsrAndMultiply) { EXPECT_EQ(matrix.Rows(), 4U); EXPECT_EQ(matrix.Columns(), 4U); - EXPECT_EQ(matrix.RowOffsets(), pattern.rowOffsets); - EXPECT_EQ(matrix.ColumnIndices(), pattern.columnIndices); + EXPECT_EQ(matrix.RowOffsets(), pattern.row_offsets); + EXPECT_EQ(matrix.ColumnIndices(), pattern.column_indices); EXPECT_EQ(matrix.Values(), (std::vector{1.0, 2.0, 3.0, 4.0, 5.0})); EXPECT_TRUE(matrix.Validate().IsOk()); @@ -122,8 +122,8 @@ TEST(SparseAssembly, PreservesExpectedStructuralZeros) { auto result = SparseMatrix::FromCoo(3U, 3U, std::move(contributions), pattern); ASSERT_TRUE(result.HasValue()); - EXPECT_EQ(result.Value().RowOffsets(), pattern.rowOffsets); - EXPECT_EQ(result.Value().ColumnIndices(), pattern.columnIndices); + EXPECT_EQ(result.Value().RowOffsets(), pattern.row_offsets); + EXPECT_EQ(result.Value().ColumnIndices(), pattern.column_indices); EXPECT_EQ(result.Value().Values(), (std::vector{2.0, 0.0, 0.0, 0.0, 0.0})); EXPECT_EQ(std::count(result.Value().Values().begin(), diff --git a/tests/unit/results/result_records_test.cpp b/tests/unit/results/result_records_test.cpp index b200580..a418ddf 100644 --- a/tests/unit/results/result_records_test.cpp +++ b/tests/unit/results/result_records_test.cpp @@ -1,5 +1,3 @@ -#include "fesa/analysis/analysis_state.hpp" - #include #include @@ -9,329 +7,281 @@ #include #include +#include "fesa/analysis/analysis_state.h" + namespace { -fesa::DofManager makeEmptyDofs() { - fesa::ModelDefinition definition{}; - definition.source_path = "models/result-records.inp"; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.steps = {{ - "Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, - {definition.source_path, 10U}}}; +fesa::DofManager MakeEmptyDofs() { + fesa::ModelDefinition definition{}; + definition.source_path = "models/result-records.inp"; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.steps = { + {"Step-1", {}, {}, 0.1, 1.0, 0.01, 1.0, {definition.source_path, 10U}}}; - auto domain = fesa::Domain::Create(std::move(definition)); - EXPECT_TRUE(domain.HasValue()); - auto model = fesa::AnalysisModel::create(domain.Value()); - EXPECT_TRUE(model.HasValue()); - auto dofs = fesa::DofManager::create(model.Value()); - EXPECT_TRUE(dofs.HasValue()); - return std::move(dofs.Value()); + auto domain = fesa::Domain::Create(std::move(definition)); + EXPECT_TRUE(domain.HasValue()); + auto model = fesa::AnalysisModel::Create(domain.Value()); + EXPECT_TRUE(model.HasValue()); + auto dofs = fesa::DofManager::Create(model.Value()); + EXPECT_TRUE(dofs.HasValue()); + return std::move(dofs.Value()); } -fesa::ShellResultRow makeShellRow( - fesa::EntityIndex element, - fesa::ShellMidsurfaceLocation location, - std::array naturalCoordinates, - double seed) { - return { - element, - location, - naturalCoordinates, - {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}, - {seed + 1.0, - seed + 2.0, - seed + 3.0, - seed + 4.0, - seed + 5.0, - seed + 6.0, - seed + 7.0, - seed + 8.0}, - {seed + 11.0, - seed + 12.0, - seed + 13.0, - seed + 14.0, - seed + 15.0, - seed + 16.0, - seed + 17.0, - seed + 18.0}, - {{{fesa::ShellSectionPosition::bottom, - -1.0, - {seed + 21.0, seed + 22.0, seed + 23.0}}, - {fesa::ShellSectionPosition::middle, - 0.0, - {seed + 24.0, seed + 25.0, seed + 26.0}}, - {fesa::ShellSectionPosition::top, - 1.0, - {seed + 27.0, seed + 28.0, seed + 29.0}}}}}; +fesa::ShellResultRow MakeShellRow(fesa::EntityIndex element, + fesa::ShellMidsurfaceLocation location, + std::array natural_coordinates, + double seed) { + return {element, + location, + natural_coordinates, + {{{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}, + {seed + 1.0, seed + 2.0, seed + 3.0, seed + 4.0, seed + 5.0, + seed + 6.0, seed + 7.0, seed + 8.0}, + {seed + 11.0, seed + 12.0, seed + 13.0, seed + 14.0, seed + 15.0, + seed + 16.0, seed + 17.0, seed + 18.0}, + {{{fesa::ShellSectionPosition::kBottom, + -1.0, + {seed + 21.0, seed + 22.0, seed + 23.0}}, + {fesa::ShellSectionPosition::kMiddle, + 0.0, + {seed + 24.0, seed + 25.0, seed + 26.0}}, + {fesa::ShellSectionPosition::kTop, + 1.0, + {seed + 27.0, seed + 28.0, seed + 29.0}}}}}; } -fesa::ShellStateCandidate makeShellCandidate( +fesa::ShellStateCandidate MakeShellCandidate( const std::vector& elements) { - const double gauss = 1.0 / std::sqrt(3.0); - const std::array locations{ - fesa::ShellMidsurfaceLocation::gp1, - fesa::ShellMidsurfaceLocation::gp2, - fesa::ShellMidsurfaceLocation::gp3, - fesa::ShellMidsurfaceLocation::gp4}; - const std::array, 4> coordinates{ - std::array{-gauss, -gauss}, - std::array{gauss, -gauss}, - std::array{gauss, gauss}, - std::array{-gauss, gauss}}; + const double gauss = 1.0 / std::sqrt(3.0); + const std::array locations{ + fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2, + fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4}; + const std::array, 4> coordinates{ + std::array{-gauss, -gauss}, + std::array{gauss, -gauss}, std::array{gauss, gauss}, + std::array{-gauss, gauss}}; - fesa::ShellStateCandidate candidate{}; - for (const auto element : elements) { - for (std::size_t point = 0U; point < locations.size(); ++point) { - candidate.rows.push_back(makeShellRow( - element, - locations[point], - coordinates[point], - 100.0 * static_cast(element) + - 10.0 * static_cast(point))); - } + fesa::ShellStateCandidate candidate{}; + for (const auto element : elements) { + for (std::size_t point = 0U; point < locations.size(); ++point) { + candidate.rows.push_back( + MakeShellRow(element, locations[point], coordinates[point], + 100.0 * static_cast(element) + + 10.0 * static_cast(point))); } - candidate.physicalStrainEnergy = 35.5; - candidate.equilibrium = {1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; - candidate.verificationMetrics = {1.0e-11, 2.0e-11, 3.0e-11}; - return candidate; + } + candidate.physical_strain_energy = 35.5; + candidate.equilibrium = {1.0, -2.0, 3.0, -4.0, 5.0, -6.0}; + candidate.verification_metrics = {1.0e-11, 2.0e-11, 3.0e-11}; + return candidate; } -void expectShellStateEquals( - const fesa::AnalysisState& state, - const std::vector& rows, - double physicalStrainEnergy, - const std::array& equilibrium, - const std::array& verificationMetrics) { - ASSERT_EQ(state.shellResults().size(), rows.size()); - for (std::size_t row = 0U; row < rows.size(); ++row) { - EXPECT_EQ(state.shellResults()[row].element, rows[row].element); - EXPECT_EQ(state.shellResults()[row].location, rows[row].location); - EXPECT_EQ( - state.shellResults()[row].naturalCoordinates, - rows[row].naturalCoordinates); - EXPECT_EQ(state.shellResults()[row].localFrame, rows[row].localFrame); - EXPECT_EQ( - state.shellResults()[row].generalizedStrain, - rows[row].generalizedStrain); - EXPECT_EQ( - state.shellResults()[row].sectionResultant, - rows[row].sectionResultant); - for (std::size_t position = 0U; - position < rows[row].stress.size(); - ++position) { - EXPECT_EQ( - state.shellResults()[row].stress[position].position, +void ExpectShellStateEquals(const fesa::AnalysisState& state, + const std::vector& rows, + double physical_strain_energy, + const std::array& equilibrium, + const std::array& verification_metrics) { + ASSERT_EQ(state.ShellResults().size(), rows.size()); + for (std::size_t row = 0U; row < rows.size(); ++row) { + EXPECT_EQ(state.ShellResults()[row].element, rows[row].element); + EXPECT_EQ(state.ShellResults()[row].location, rows[row].location); + EXPECT_EQ(state.ShellResults()[row].natural_coordinates, + rows[row].natural_coordinates); + EXPECT_EQ(state.ShellResults()[row].local_frame, rows[row].local_frame); + EXPECT_EQ(state.ShellResults()[row].generalized_strain, + rows[row].generalized_strain); + EXPECT_EQ(state.ShellResults()[row].section_resultant, + rows[row].section_resultant); + for (std::size_t position = 0U; position < rows[row].stress.size(); + ++position) { + EXPECT_EQ(state.ShellResults()[row].stress[position].position, rows[row].stress[position].position); - EXPECT_DOUBLE_EQ( - state.shellResults()[row].stress[position].zeta, - rows[row].stress[position].zeta); - EXPECT_EQ( - state.shellResults()[row].stress[position].components, + EXPECT_DOUBLE_EQ(state.ShellResults()[row].stress[position].zeta, + rows[row].stress[position].zeta); + EXPECT_EQ(state.ShellResults()[row].stress[position].components, rows[row].stress[position].components); - } } - EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), physicalStrainEnergy); - EXPECT_EQ(state.equilibrium(), equilibrium); - EXPECT_EQ(state.verificationMetrics(), verificationMetrics); + } + EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), physical_strain_energy); + EXPECT_EQ(state.Equilibrium(), equilibrium); + EXPECT_EQ(state.VerificationMetrics(), verification_metrics); } -void expectShellCandidateRejectedWithoutMutation( +void ExpectShellCandidateRejectedWithoutMutation( fesa::AnalysisState& state, - const std::vector& expectedElements, + const std::vector& expected_elements, const fesa::ShellStateCandidate& candidate, const fesa::ShellStateCandidate& committed) { - const auto status = state.commitShellResults(expectedElements, candidate); - EXPECT_FALSE(status.IsOk()); - EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); - expectShellStateEquals( - state, - committed.rows, - committed.physicalStrainEnergy, - committed.equilibrium, - committed.verificationMetrics); - ASSERT_TRUE(state.commitShellResults(expectedElements, committed).IsOk()); + const auto status = state.CommitShellResults(expected_elements, candidate); + EXPECT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); + ExpectShellStateEquals(state, committed.rows, + committed.physical_strain_energy, + committed.equilibrium, committed.verification_metrics); + ASSERT_TRUE(state.CommitShellResults(expected_elements, committed).IsOk()); } -} // namespace +} // namespace TEST(AnalysisState, PreservesStepFrameAndStableRowOrder) { - const auto dofs = makeEmptyDofs(); - auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); + const auto dofs = MakeEmptyDofs(); + auto state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); - const fesa::EndpointResultRow firstEndpoint{ - 2U, - -1, - {"Beam-1", 10, "010"}, - {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, - {7.0, 8.0, 9.0, 10.0}}; - const fesa::EndpointResultRow secondEndpoint{ - 2U, - 1, - {"Beam-1", 20, "020"}, - {11.0, 12.0, 13.0, 14.0, 15.0, 16.0}, - {17.0, 18.0, 19.0, 20.0}}; - const fesa::GaussResultRow firstGauss{ - 2U, 1, {0.1, 0.2, 0.3, 0.4}, {1.1, 1.2, 1.3, 1.4}}; - const fesa::GaussResultRow secondGauss{ - 2U, 2, {0.5, 0.6, 0.7, 0.8}, {1.5, 1.6, 1.7, 1.8}}; - const fesa::StressS11Row firstStress{ - 2U, 1, 1U, -0.25, 0.5, 12.5, "input"}; - const fesa::StressS11Row secondStress{ - 2U, 1, 2U, 0.25, -0.5, -7.5, "input"}; + const fesa::EndpointResultRow first_endpoint{2U, + -1, + {"Beam-1", 10, "010"}, + {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, + {7.0, 8.0, 9.0, 10.0}}; + const fesa::EndpointResultRow second_endpoint{ + 2U, + 1, + {"Beam-1", 20, "020"}, + {11.0, 12.0, 13.0, 14.0, 15.0, 16.0}, + {17.0, 18.0, 19.0, 20.0}}; + const fesa::GaussResultRow first_gauss{ + 2U, 1, {0.1, 0.2, 0.3, 0.4}, {1.1, 1.2, 1.3, 1.4}}; + const fesa::GaussResultRow second_gauss{ + 2U, 2, {0.5, 0.6, 0.7, 0.8}, {1.5, 1.6, 1.7, 1.8}}; + const fesa::StressS11Row first_stress{2U, 1, 1U, -0.25, 0.5, 12.5, "input"}; + const fesa::StressS11Row second_stress{2U, 1, 2U, 0.25, -0.5, -7.5, "input"}; - state.endpointResults().push_back(firstEndpoint); - state.endpointResults().push_back(secondEndpoint); - state.gaussResults().push_back(firstGauss); - state.gaussResults().push_back(secondGauss); - state.stressResults().push_back(firstStress); - state.stressResults().push_back(secondStress); + state.EndpointResults().push_back(first_endpoint); + state.EndpointResults().push_back(second_endpoint); + state.GaussResults().push_back(first_gauss); + state.GaussResults().push_back(second_gauss); + state.StressResults().push_back(first_stress); + state.StressResults().push_back(second_stress); - const auto* const endpointStorage = state.endpointResults().data(); - const auto* const gaussStorage = state.gaussResults().data(); - const auto* const stressStorage = state.stressResults().data(); - const fesa::AnalysisState& constState = state; + const auto* const endpoint_storage = state.EndpointResults().data(); + const auto* const gauss_storage = state.GaussResults().data(); + const auto* const stress_storage = state.StressResults().data(); + const fesa::AnalysisState& const_state = state; - EXPECT_EQ(constState.identity().stepName, "Step-1"); - EXPECT_EQ(constState.identity().frameIndex, 0U); - ASSERT_EQ(constState.endpointResults().size(), 2U); - EXPECT_EQ(constState.endpointResults().data(), endpointStorage); - EXPECT_EQ(constState.endpointResults()[0].element, 2U); - EXPECT_EQ(constState.endpointResults()[0].endpoint, -1); - EXPECT_EQ(constState.endpointResults()[0].node.instance_name, "Beam-1"); - EXPECT_EQ(constState.endpointResults()[0].node.source_label, 10); - EXPECT_EQ(constState.endpointResults()[0].node.source_label_text, "010"); - EXPECT_EQ( - constState.endpointResults()[0].endAction, - (std::array{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})); - EXPECT_EQ( - constState.endpointResults()[1].sectionResultant, - (std::array{17.0, 18.0, 19.0, 20.0})); + EXPECT_EQ(const_state.Identity().step_name, "Step-1"); + EXPECT_EQ(const_state.Identity().frame_index, 0U); + ASSERT_EQ(const_state.EndpointResults().size(), 2U); + EXPECT_EQ(const_state.EndpointResults().data(), endpoint_storage); + EXPECT_EQ(const_state.EndpointResults()[0].element, 2U); + EXPECT_EQ(const_state.EndpointResults()[0].endpoint, -1); + EXPECT_EQ(const_state.EndpointResults()[0].node.instance_name, "Beam-1"); + EXPECT_EQ(const_state.EndpointResults()[0].node.source_label, 10); + EXPECT_EQ(const_state.EndpointResults()[0].node.source_label_text, "010"); + EXPECT_EQ(const_state.EndpointResults()[0].end_action, + (std::array{1.0, 2.0, 3.0, 4.0, 5.0, 6.0})); + EXPECT_EQ(const_state.EndpointResults()[1].section_resultant, + (std::array{17.0, 18.0, 19.0, 20.0})); - ASSERT_EQ(constState.gaussResults().size(), 2U); - EXPECT_EQ(constState.gaussResults().data(), gaussStorage); - EXPECT_EQ(constState.gaussResults()[0].gaussPoint, 1); - EXPECT_EQ(constState.gaussResults()[1].gaussPoint, 2); - EXPECT_EQ( - constState.gaussResults()[0].generalizedStrain, - (std::array{0.1, 0.2, 0.3, 0.4})); - EXPECT_EQ( - constState.gaussResults()[1].generalizedResultant, - (std::array{1.5, 1.6, 1.7, 1.8})); + ASSERT_EQ(const_state.GaussResults().size(), 2U); + EXPECT_EQ(const_state.GaussResults().data(), gauss_storage); + EXPECT_EQ(const_state.GaussResults()[0].gauss_point, 1); + EXPECT_EQ(const_state.GaussResults()[1].gauss_point, 2); + EXPECT_EQ(const_state.GaussResults()[0].generalized_strain, + (std::array{0.1, 0.2, 0.3, 0.4})); + EXPECT_EQ(const_state.GaussResults()[1].generalized_resultant, + (std::array{1.5, 1.6, 1.7, 1.8})); - ASSERT_EQ(constState.stressResults().size(), 2U); - EXPECT_EQ(constState.stressResults().data(), stressStorage); - EXPECT_EQ(constState.stressResults()[0].sectionPoint, 1U); - EXPECT_DOUBLE_EQ(constState.stressResults()[0].x1, -0.25); - EXPECT_DOUBLE_EQ(constState.stressResults()[0].x2, 0.5); - EXPECT_DOUBLE_EQ(constState.stressResults()[0].s11, 12.5); - EXPECT_EQ(constState.stressResults()[0].source, "input"); - EXPECT_EQ(constState.stressResults()[1].sectionPoint, 2U); + ASSERT_EQ(const_state.StressResults().size(), 2U); + EXPECT_EQ(const_state.StressResults().data(), stress_storage); + EXPECT_EQ(const_state.StressResults()[0].section_point, 1U); + EXPECT_DOUBLE_EQ(const_state.StressResults()[0].x1, -0.25); + EXPECT_DOUBLE_EQ(const_state.StressResults()[0].x2, 0.5); + EXPECT_DOUBLE_EQ(const_state.StressResults()[0].s11, 12.5); + EXPECT_EQ(const_state.StressResults()[0].source, "input"); + EXPECT_EQ(const_state.StressResults()[1].section_point, 2U); } // MITC4-STATE-001 TEST(AnalysisState, OwnsExactShellRowsInStableElementAndLocationOrder) { - const auto dofs = makeEmptyDofs(); - auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); - const std::vector expectedElements{3U, 7U}; - auto candidate = makeShellCandidate(expectedElements); + const auto dofs = MakeEmptyDofs(); + auto state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); + const std::vector expected_elements{3U, 7U}; + auto candidate = MakeShellCandidate(expected_elements); - const auto status = state.commitShellResults(expectedElements, candidate); + const auto status = state.CommitShellResults(expected_elements, candidate); - ASSERT_TRUE(status.IsOk()); - const fesa::AnalysisState& constState = state; - ASSERT_EQ(constState.shellResults().size(), 8U); - EXPECT_EQ(constState.shellResults()[0].element, 3U); - EXPECT_EQ( - constState.shellResults()[0].location, - fesa::ShellMidsurfaceLocation::gp1); - EXPECT_EQ( - constState.shellResults()[3].location, - fesa::ShellMidsurfaceLocation::gp4); - EXPECT_EQ(constState.shellResults()[4].element, 7U); - EXPECT_EQ( - constState.shellResults()[4].location, - fesa::ShellMidsurfaceLocation::gp1); - EXPECT_EQ( - constState.shellResults()[0].naturalCoordinates, - (std::array{ - -1.0 / std::sqrt(3.0), -1.0 / std::sqrt(3.0)})); - EXPECT_EQ( - constState.shellResults()[2].generalizedStrain, - (std::array{ - 321.0, 322.0, 323.0, 324.0, - 325.0, 326.0, 327.0, 328.0})); - EXPECT_EQ( - constState.shellResults()[7].sectionResultant, - (std::array{ - 741.0, 742.0, 743.0, 744.0, - 745.0, 746.0, 747.0, 748.0})); - EXPECT_EQ( - constState.shellResults()[7].stress[0].position, - fesa::ShellSectionPosition::bottom); - EXPECT_DOUBLE_EQ(constState.shellResults()[7].stress[0].zeta, -1.0); - EXPECT_EQ( - constState.shellResults()[7].stress[2].components, - (std::array{757.0, 758.0, 759.0})); + ASSERT_TRUE(status.IsOk()); + const fesa::AnalysisState& const_state = state; + ASSERT_EQ(const_state.ShellResults().size(), 8U); + EXPECT_EQ(const_state.ShellResults()[0].element, 3U); + EXPECT_EQ(const_state.ShellResults()[0].location, + fesa::ShellMidsurfaceLocation::kGp1); + EXPECT_EQ(const_state.ShellResults()[3].location, + fesa::ShellMidsurfaceLocation::kGp4); + EXPECT_EQ(const_state.ShellResults()[4].element, 7U); + EXPECT_EQ(const_state.ShellResults()[4].location, + fesa::ShellMidsurfaceLocation::kGp1); + EXPECT_EQ( + const_state.ShellResults()[0].natural_coordinates, + (std::array{-1.0 / std::sqrt(3.0), -1.0 / std::sqrt(3.0)})); + EXPECT_EQ(const_state.ShellResults()[2].generalized_strain, + (std::array{321.0, 322.0, 323.0, 324.0, 325.0, 326.0, + 327.0, 328.0})); + EXPECT_EQ(const_state.ShellResults()[7].section_resultant, + (std::array{741.0, 742.0, 743.0, 744.0, 745.0, 746.0, + 747.0, 748.0})); + EXPECT_EQ(const_state.ShellResults()[7].stress[0].position, + fesa::ShellSectionPosition::kBottom); + EXPECT_DOUBLE_EQ(const_state.ShellResults()[7].stress[0].zeta, -1.0); + EXPECT_EQ(const_state.ShellResults()[7].stress[2].components, + (std::array{757.0, 758.0, 759.0})); } // MITC4-STATE-002 TEST(AnalysisState, CommitsFiniteShellGlobalEvidence) { - const auto dofs = makeEmptyDofs(); - auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); - const std::vector expectedElements{5U}; - const auto candidate = makeShellCandidate(expectedElements); + const auto dofs = MakeEmptyDofs(); + auto state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); + const std::vector expected_elements{5U}; + const auto candidate = MakeShellCandidate(expected_elements); - const auto status = state.commitShellResults(expectedElements, candidate); + const auto status = state.CommitShellResults(expected_elements, candidate); - ASSERT_TRUE(status.IsOk()); - EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), 35.5); - EXPECT_EQ( - state.equilibrium(), - (std::array{1.0, -2.0, 3.0, -4.0, 5.0, -6.0})); - EXPECT_EQ( - state.verificationMetrics(), - (std::array{1.0e-11, 2.0e-11, 3.0e-11})); + ASSERT_TRUE(status.IsOk()); + EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 35.5); + EXPECT_EQ(state.Equilibrium(), + (std::array{1.0, -2.0, 3.0, -4.0, 5.0, -6.0})); + EXPECT_EQ(state.VerificationMetrics(), + (std::array{1.0e-11, 2.0e-11, 3.0e-11})); } // MITC4-STATE-003 TEST(AnalysisState, InvalidShellCandidatesLeavePriorStateUnchanged) { - const auto dofs = makeEmptyDofs(); - auto state = fesa::AnalysisState::create(dofs, {"Step-1", 0U}); - const std::vector expectedElements{5U}; - const auto committed = makeShellCandidate(expectedElements); - ASSERT_TRUE(state.commitShellResults(expectedElements, committed).IsOk()); + const auto dofs = MakeEmptyDofs(); + auto state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U}); + const std::vector expected_elements{5U}; + const auto committed = MakeShellCandidate(expected_elements); + ASSERT_TRUE(state.CommitShellResults(expected_elements, committed).IsOk()); - auto invalidLocation = makeShellCandidate(expectedElements); - invalidLocation.rows[0].location = fesa::ShellMidsurfaceLocation::gp2; - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, invalidLocation, committed); + auto invalid_location = MakeShellCandidate(expected_elements); + invalid_location.rows[0].location = fesa::ShellMidsurfaceLocation::kGp2; + ExpectShellCandidateRejectedWithoutMutation(state, expected_elements, + invalid_location, committed); - auto nonfinite = makeShellCandidate(expectedElements); - nonfinite.rows[2].generalizedStrain[6] = - (std::numeric_limits::quiet_NaN)(); - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, nonfinite, committed); + auto nonfinite = MakeShellCandidate(expected_elements); + nonfinite.rows[2].generalized_strain[6] = + (std::numeric_limits::quiet_NaN)(); + ExpectShellCandidateRejectedWithoutMutation(state, expected_elements, + nonfinite, committed); - auto nonfiniteFrame = makeShellCandidate(expectedElements); - nonfiniteFrame.rows[1].localFrame[2][0] = - (std::numeric_limits::infinity)(); - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, nonfiniteFrame, committed); + auto nonfinite_frame = MakeShellCandidate(expected_elements); + nonfinite_frame.rows[1].local_frame[2][0] = + (std::numeric_limits::infinity)(); + ExpectShellCandidateRejectedWithoutMutation(state, expected_elements, + nonfinite_frame, committed); - auto invalidSectionPosition = makeShellCandidate(expectedElements); - invalidSectionPosition.rows[3].stress[0].position = - fesa::ShellSectionPosition::top; - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, invalidSectionPosition, committed); + auto invalid_section_position = MakeShellCandidate(expected_elements); + invalid_section_position.rows[3].stress[0].position = + fesa::ShellSectionPosition::kTop; + ExpectShellCandidateRejectedWithoutMutation( + state, expected_elements, invalid_section_position, committed); - auto nonfiniteGlobalEvidence = makeShellCandidate(expectedElements); - nonfiniteGlobalEvidence.verificationMetrics[1] = - (std::numeric_limits::infinity)(); - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, nonfiniteGlobalEvidence, committed); + auto nonfinite_global_evidence = MakeShellCandidate(expected_elements); + nonfinite_global_evidence.verification_metrics[1] = + (std::numeric_limits::infinity)(); + ExpectShellCandidateRejectedWithoutMutation( + state, expected_elements, nonfinite_global_evidence, committed); - auto incompleteInventory = makeShellCandidate(expectedElements); - incompleteInventory.rows.pop_back(); - expectShellCandidateRejectedWithoutMutation( - state, expectedElements, incompleteInventory, committed); + auto incomplete_inventory = MakeShellCandidate(expected_elements); + incomplete_inventory.rows.pop_back(); + ExpectShellCandidateRejectedWithoutMutation(state, expected_elements, + incomplete_inventory, committed); } diff --git a/tests/unit/results/result_recovery_test.cpp b/tests/unit/results/result_recovery_test.cpp index 6e55c9f..487eb60 100644 --- a/tests/unit/results/result_recovery_test.cpp +++ b/tests/unit/results/result_recovery_test.cpp @@ -1,12 +1,4 @@ -#include "fesa/results/result_recovery.hpp" - -#include "fesa/analysis/analysis_model.hpp" -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/assembly/load_assembler.hpp" -#include "fesa/assembly/parallel_for.hpp" -#include "fesa/assembly/sparse_assembler.hpp" -#include "fesa/fem/dof_manager.hpp" -#include "fesa/model/domain.h" +#include "fesa/results/result_recovery.h" #include @@ -22,6 +14,14 @@ #include #include +#include "fesa/analysis/analysis_model.h" +#include "fesa/analysis/analysis_state.h" +#include "fesa/assembly/load_assembler.h" +#include "fesa/assembly/parallel_for.h" +#include "fesa/assembly/sparse_assembler.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + namespace { constexpr double kYoungsModulus = 100.0; @@ -29,962 +29,836 @@ constexpr double kPoissonRatio = 0.25; constexpr double kLength = 2.0; struct RecoveryFixture { - std::unique_ptr domain; - std::unique_ptr model; - std::unique_ptr dofs; - std::unique_ptr stiffness; + std::unique_ptr domain; + std::unique_ptr model; + std::unique_ptr dofs; + std::unique_ptr stiffness; }; struct ShellRecoveryFixture { - std::unique_ptr domain; - std::unique_ptr model; - std::unique_ptr dofs; - std::unique_ptr stiffness; + std::unique_ptr domain; + std::unique_ptr model; + std::unique_ptr dofs; + std::unique_ptr stiffness; }; -fesa::ModelDefinition makeDefinition( - const bool twoElements = false, - std::vector> sectionPoints = {}, - std::vector loads = {}, - const bool reverseSecond = false, - const bool sectionJump = false, - const bool nonzeroPrescription = true) { - const std::filesystem::path source{"models/result-recovery.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:0123456789abcdef"; - definition.nodes = { - {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, - {{"Beam-1", 2, "2"}, {kLength, 0.0, 0.0}, {source, 11U}}}; - if (twoElements) { - definition.nodes.push_back( - {{"Beam-1", 3, "3"}, {2.0 * kLength, 0.0, 0.0}, {source, 12U}}); - } - definition.materials = { - {"Material", kYoungsModulus, kPoissonRatio, {source, 20U}}}; - definition.sections = {{ - "Section", - 2.0, - 3.0, - 0.0, - 4.0, - 5.0, - {0.0, 1.0, 0.0}, - std::move(sectionPoints), - {source, 30U}}}; - if (sectionJump) { - auto secondSection = definition.sections.front(); - secondSection.name = "Section-2"; - secondSection.area = 2.5; - secondSection.location.line = 31U; - definition.sections.push_back(std::move(secondSection)); - } - definition.elements = { - {{"Beam-1", 10, "10"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; - if (twoElements) { - definition.elements.push_back({ - {"Beam-1", 20, "20"}, - reverseSecond ? std::array{2U, 1U} - : std::array{1U, 2U}, - 0U, - sectionJump ? 1U : 0U, - {source, 41U}}); - } - definition.steps = {{ - "Step-1", - {{"1", 1, 1, nonzeroPrescription ? 0.1 : 0.0, {source, 50U}}, - {"1", 2, 6, 0.0, {source, 51U}}}, - std::move(loads), - 0.1, - 1.0, - 0.01, - 1.0, - {source, 49U}}}; - return definition; +fesa::ModelDefinition MakeDefinition( + const bool two_elements = false, + std::vector> section_points = {}, + std::vector loads = {}, const bool reverse_second = false, + const bool section_jump = false, const bool nonzero_prescription = true) { + const std::filesystem::path source{"models/result-recovery.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:0123456789abcdef"; + definition.nodes = {{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, + {{"Beam-1", 2, "2"}, {kLength, 0.0, 0.0}, {source, 11U}}}; + if (two_elements) { + definition.nodes.push_back( + {{"Beam-1", 3, "3"}, {2.0 * kLength, 0.0, 0.0}, {source, 12U}}); + } + definition.materials = { + {"Material", kYoungsModulus, kPoissonRatio, {source, 20U}}}; + definition.sections = {{"Section", + 2.0, + 3.0, + 0.0, + 4.0, + 5.0, + {0.0, 1.0, 0.0}, + std::move(section_points), + {source, 30U}}}; + if (section_jump) { + auto second_section = definition.sections.front(); + second_section.name = "Section-2"; + second_section.area = 2.5; + second_section.location.line = 31U; + definition.sections.push_back(std::move(second_section)); + } + definition.elements = { + {{"Beam-1", 10, "10"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; + if (two_elements) { + definition.elements.push_back( + {{"Beam-1", 20, "20"}, + reverse_second ? std::array{2U, 1U} + : std::array{1U, 2U}, + 0U, + section_jump ? 1U : 0U, + {source, 41U}}); + } + definition.steps = { + {"Step-1", + {{"1", 1, 1, nonzero_prescription ? 0.1 : 0.0, {source, 50U}}, + {"1", 2, 6, 0.0, {source, 51U}}}, + std::move(loads), + 0.1, + 1.0, + 0.01, + 1.0, + {source, 49U}}}; + return definition; } -RecoveryFixture makeFixture( - const bool twoElements = false, - std::vector> sectionPoints = {}, - std::vector loads = {}, - const bool reverseSecond = false, - const bool sectionJump = false, - const bool nonzeroPrescription = true) { - auto domainResult = fesa::Domain::Create(makeDefinition( - twoElements, - std::move(sectionPoints), - std::move(loads), - reverseSecond, - sectionJump, - nonzeroPrescription)); - if (!domainResult.HasValue()) { - throw std::runtime_error{"Recovery fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); +RecoveryFixture MakeFixture( + const bool two_elements = false, + std::vector> section_points = {}, + std::vector loads = {}, const bool reverse_second = false, + const bool section_jump = false, const bool nonzero_prescription = true) { + auto domain_result = fesa::Domain::Create( + MakeDefinition(two_elements, std::move(section_points), std::move(loads), + reverse_second, section_jump, nonzero_prescription)); + if (!domain_result.HasValue()) { + throw std::runtime_error{"Recovery fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{"Recovery fixture AnalysisModel construction failed."}; - } - auto model = std::make_unique( - std::move(modelResult.Value())); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Recovery fixture AnalysisModel construction failed."}; + } + auto model = + std::make_unique(std::move(model_result.Value())); - auto dofsResult = fesa::DofManager::create(*model); - if (!dofsResult.HasValue()) { - throw std::runtime_error{"Recovery fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofsResult.Value())); + auto dofs_result = fesa::DofManager::Create(*model); + if (!dofs_result.HasValue()) { + throw std::runtime_error{ + "Recovery fixture DofManager construction failed."}; + } + auto dofs = + std::make_unique(std::move(dofs_result.Value())); - fesa::SerialParallelFor serial; - auto stiffnessResult = fesa::SparseAssembler::assembleStiffness( - *model, *dofs, serial); - if (!stiffnessResult.HasValue()) { - throw std::runtime_error{"Recovery fixture stiffness assembly failed."}; - } - auto stiffness = std::make_unique( - std::move(stiffnessResult.Value())); - return { - std::move(domain), - std::move(model), - std::move(dofs), - std::move(stiffness)}; + fesa::SerialParallelFor serial; + auto stiffness_result = + fesa::SparseAssembler::AssembleStiffness(*model, *dofs, serial); + if (!stiffness_result.HasValue()) { + throw std::runtime_error{"Recovery fixture stiffness assembly failed."}; + } + auto stiffness = + std::make_unique(std::move(stiffness_result.Value())); + return {std::move(domain), std::move(model), std::move(dofs), + std::move(stiffness)}; } -fesa::ModelDefinition makeShellDefinition( - const bool twoElements = false, - const bool constrainAll = false, +fesa::ModelDefinition MakeShellDefinition( + const bool two_elements = false, const bool constrain_all = false, std::vector loads = {}) { - const std::filesystem::path source{"models/shell-result-recovery.inp"}; - fesa::ModelDefinition definition{}; - definition.source_path = source; - definition.source_content_identity = "fnv1a64:fedcba9876543210"; - definition.nodes = { - {{"Shell-1", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 10U}}, - {{"Shell-1", 2, "2"}, {1.0, -1.0, 0.0}, {source, 11U}}, - {{"Shell-1", 3, "3"}, {3.0, -1.0, 0.0}, {source, 12U}}, - {{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 13U}}, - {{"Shell-1", 5, "5"}, {1.0, 1.0, 0.0}, {source, 14U}}, - {{"Shell-1", 6, "6"}, {3.0, 1.0, 0.0}, {source, 15U}}}; - if (!twoElements) { - definition.nodes.erase( - definition.nodes.begin() + 2, - definition.nodes.begin() + 3); - definition.nodes.erase(definition.nodes.begin() + 4); - } - definition.materials = { - {"Material", 120.0, 0.25, {source, 20U}}}; - definition.shell_sections = { - {"ShellSection", 2.0, 0U, {source, 30U}}}; - definition.shell_elements = {{ - {"Shell-1", 10, "10"}, - fesa::ShellSourceElementType::kS4, - {0U, 1U, twoElements ? 4U : 3U, twoElements ? 3U : 2U}, - 0U, - 0U, - {source, 40U}}}; - if (twoElements) { - definition.shell_elements.push_back({ - {"Shell-1", 20, "20"}, - fesa::ShellSourceElementType::kS4r, - {1U, 2U, 5U, 4U}, - 0U, - 0U, - {source, 41U}}); - } + const std::filesystem::path source{"models/shell-result-recovery.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:fedcba9876543210"; + definition.nodes = {{{"Shell-1", 1, "1"}, {-1.0, -1.0, 0.0}, {source, 10U}}, + {{"Shell-1", 2, "2"}, {1.0, -1.0, 0.0}, {source, 11U}}, + {{"Shell-1", 3, "3"}, {3.0, -1.0, 0.0}, {source, 12U}}, + {{"Shell-1", 4, "4"}, {-1.0, 1.0, 0.0}, {source, 13U}}, + {{"Shell-1", 5, "5"}, {1.0, 1.0, 0.0}, {source, 14U}}, + {{"Shell-1", 6, "6"}, {3.0, 1.0, 0.0}, {source, 15U}}}; + if (!two_elements) { + definition.nodes.erase(definition.nodes.begin() + 2, + definition.nodes.begin() + 3); + definition.nodes.erase(definition.nodes.begin() + 4); + } + definition.materials = {{"Material", 120.0, 0.25, {source, 20U}}}; + definition.shell_sections = {{"ShellSection", 2.0, 0U, {source, 30U}}}; + definition.shell_elements = { + {{"Shell-1", 10, "10"}, + fesa::ShellSourceElementType::kS4, + {0U, 1U, two_elements ? 4U : 3U, two_elements ? 3U : 2U}, + 0U, + 0U, + {source, 40U}}}; + if (two_elements) { + definition.shell_elements.push_back({{"Shell-1", 20, "20"}, + fesa::ShellSourceElementType::kS4r, + {1U, 2U, 5U, 4U}, + 0U, + 0U, + {source, 41U}}); + } + 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}}); + } + if (constrain_all) { + std::vector all_nodes; + all_nodes.reserve(definition.nodes.size()); 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}}); + all_nodes.push_back(static_cast(node)); } - if (constrainAll) { - std::vector allNodes; - allNodes.reserve(definition.nodes.size()); - for (std::size_t node = 0U; node < definition.nodes.size(); ++node) { - allNodes.push_back(static_cast(node)); - } - definition.node_sets.push_back( - {"All", {}, std::move(allNodes), {source, 50U}}); - } - definition.steps = {{ - "Step-1", - constrainAll - ? std::vector{ - {"All", 1, 6, 0.0, {source, 60U}}} - : std::vector{}, - std::move(loads), - 0.1, - 1.0, - 0.01, - 1.0, - {source, 59U}}}; - return definition; + definition.node_sets.push_back( + {"All", {}, std::move(all_nodes), {source, 50U}}); + } + definition.steps = { + {"Step-1", + constrain_all ? std::vector{{"All", + 1, + 6, + 0.0, + {source, 60U}}} + : std::vector{}, + std::move(loads), + 0.1, + 1.0, + 0.01, + 1.0, + {source, 59U}}}; + return definition; } -ShellRecoveryFixture makeShellFixture(fesa::ModelDefinition definition) { - auto domainResult = fesa::Domain::Create(std::move(definition)); - if (!domainResult.HasValue()) { - throw std::runtime_error{ - "Shell recovery fixture Domain construction failed."}; - } - auto domain = std::make_unique( - std::move(domainResult.Value())); +ShellRecoveryFixture MakeShellFixture(fesa::ModelDefinition definition) { + auto domain_result = fesa::Domain::Create(std::move(definition)); + if (!domain_result.HasValue()) { + throw std::runtime_error{ + "Shell recovery fixture Domain construction failed."}; + } + auto domain = + std::make_unique(std::move(domain_result.Value())); - auto modelResult = fesa::AnalysisModel::create(*domain); - if (!modelResult.HasValue()) { - throw std::runtime_error{ - "Shell recovery fixture AnalysisModel construction failed."}; - } - auto model = std::make_unique( - std::move(modelResult.Value())); + auto model_result = fesa::AnalysisModel::Create(*domain); + if (!model_result.HasValue()) { + throw std::runtime_error{ + "Shell recovery fixture AnalysisModel construction failed."}; + } + auto model = + std::make_unique(std::move(model_result.Value())); - auto dofsResult = fesa::DofManager::create(*model); - if (!dofsResult.HasValue()) { - throw std::runtime_error{ - "Shell recovery fixture DofManager construction failed."}; - } - auto dofs = std::make_unique( - std::move(dofsResult.Value())); + auto dofs_result = fesa::DofManager::Create(*model); + if (!dofs_result.HasValue()) { + throw std::runtime_error{ + "Shell recovery fixture DofManager construction failed."}; + } + auto dofs = + std::make_unique(std::move(dofs_result.Value())); - fesa::SerialParallelFor serial; - auto stiffnessResult = fesa::SparseAssembler::assembleStiffness( - *model, *dofs, serial); - if (!stiffnessResult.HasValue()) { - throw std::runtime_error{ - "Shell recovery fixture stiffness assembly failed."}; - } - auto stiffness = std::make_unique( - std::move(stiffnessResult.Value())); - return { - std::move(domain), - std::move(model), - std::move(dofs), - std::move(stiffness)}; + fesa::SerialParallelFor serial; + auto stiffness_result = + fesa::SparseAssembler::AssembleStiffness(*model, *dofs, serial); + if (!stiffness_result.HasValue()) { + throw std::runtime_error{ + "Shell recovery fixture stiffness assembly failed."}; + } + auto stiffness = + std::make_unique(std::move(stiffness_result.Value())); + return {std::move(domain), std::move(model), std::move(dofs), + std::move(stiffness)}; } -fesa::AnalysisState makeShellPhysicalState( +fesa::AnalysisState MakeShellPhysicalState( const ShellRecoveryFixture& fixture) { - constexpr std::array generalized{ - 0.1, -0.05, 0.2, 0.3, -0.15, 0.25, 0.4, -0.3}; - auto state = fesa::AnalysisState::create( - *fixture.dofs, {"Step-1", 0U}); - for (std::size_t node = 0U; - node < fixture.domain->Nodes().size(); - ++node) { - const double x = fixture.domain->Nodes()[node].coordinates[0U]; - const double y = fixture.domain->Nodes()[node].coordinates[1U]; - const std::size_t offset = 6U * node; - state.displacement()[offset] = - generalized[0U] * x + 0.5 * generalized[2U] * y; - state.displacement()[offset + 1U] = - generalized[1U] * y + 0.5 * generalized[2U] * x; - state.displacement()[offset + 2U] = - generalized[6U] * x + generalized[7U] * y - - 0.5 * generalized[5U] * x * y; - state.displacement()[offset + 3U] = - -generalized[4U] * y - 0.5 * generalized[5U] * x; - state.displacement()[offset + 4U] = - generalized[3U] * x + 0.5 * generalized[5U] * y; - } - state.externalForce() = - fixture.stiffness->Multiply(state.displacement()); - return state; + constexpr std::array generalized{0.1, -0.05, 0.2, 0.3, + -0.15, 0.25, 0.4, -0.3}; + auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) { + const double x = fixture.domain->Nodes()[node].coordinates[0U]; + const double y = fixture.domain->Nodes()[node].coordinates[1U]; + const std::size_t offset = 6U * node; + state.Displacement()[offset] = + generalized[0U] * x + 0.5 * generalized[2U] * y; + state.Displacement()[offset + 1U] = + generalized[1U] * y + 0.5 * generalized[2U] * x; + state.Displacement()[offset + 2U] = generalized[6U] * x + + generalized[7U] * y - + 0.5 * generalized[5U] * x * y; + state.Displacement()[offset + 3U] = + -generalized[4U] * y - 0.5 * generalized[5U] * x; + state.Displacement()[offset + 4U] = + generalized[3U] * x + 0.5 * generalized[5U] * y; + } + state.ExternalForce() = fixture.stiffness->Multiply(state.Displacement()); + return state; } -fesa::AnalysisState makeAxialEquilibriumState(const RecoveryFixture& fixture) { - auto state = fesa::AnalysisState::create( - *fixture.dofs, {"Step-1", 0U}); - state.displacement()[0U] = 0.1; - state.displacement()[6U] = 0.3; - const fesa::Vector internal = fixture.stiffness->Multiply(state.displacement()); - for (const std::size_t fullDof : fixture.dofs->freeDofs()) { - state.externalForce()[fullDof] = internal[fullDof]; - } - return state; +fesa::AnalysisState MakeAxialEquilibriumState(const RecoveryFixture& fixture) { + auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + state.Displacement()[0U] = 0.1; + state.Displacement()[6U] = 0.3; + const fesa::Vector internal = + fixture.stiffness->Multiply(state.Displacement()); + for (const std::size_t full_dof : fixture.dofs->FreeDofs()) { + state.ExternalForce()[full_dof] = internal[full_dof]; + } + return state; } -fesa::AnalysisState makePatchState( - const RecoveryFixture& fixture, - const double epsilon, - const double twist, - const double kappaY, - const double kappaZ) { - auto state = fesa::AnalysisState::create( - *fixture.dofs, {"Step-1", 0U}); - state.displacement()[0U] = 0.1; - state.displacement()[6U] = 0.1 + epsilon * kLength; - state.displacement()[7U] = 0.5 * kappaZ * kLength * kLength; - state.displacement()[8U] = -0.5 * kappaY * kLength * kLength; - state.displacement()[9U] = twist * kLength; - state.displacement()[10U] = kappaY * kLength; - state.displacement()[11U] = kappaZ * kLength; - state.externalForce() = fixture.stiffness->Multiply(state.displacement()); - return state; +fesa::AnalysisState MakePatchState(const RecoveryFixture& fixture, + const double epsilon, const double twist, + const double kappa_y, const double kappa_z) { + auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + state.Displacement()[0U] = 0.1; + state.Displacement()[6U] = 0.1 + epsilon * kLength; + state.Displacement()[7U] = 0.5 * kappa_z * kLength * kLength; + state.Displacement()[8U] = -0.5 * kappa_y * kLength * kLength; + state.Displacement()[9U] = twist * kLength; + state.Displacement()[10U] = kappa_y * kLength; + state.Displacement()[11U] = kappa_z * kLength; + state.ExternalForce() = fixture.stiffness->Multiply(state.Displacement()); + return state; } -void expectStatusCode(const fesa::Status& status, const std::string& code) { - ASSERT_FALSE(status.IsOk()); - EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); - ASSERT_EQ(status.Diagnostics().size(), 1U); - EXPECT_EQ(status.Diagnostics()[0U].code, code); +void ExpectStatusCode(const fesa::Status& status, const std::string& code) { + ASSERT_FALSE(status.IsOk()); + EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, code); } -void expectScaledNear( - const double actual, - const double expected, - const double relativeTolerance = 1.0e-12) { - ASSERT_TRUE(std::isfinite(actual)); - ASSERT_TRUE(std::isfinite(expected)); - EXPECT_LE( - std::abs(actual - expected), - relativeTolerance * (std::max)(std::abs(expected), 1.0)); +void ExpectScaledNear(const double actual, const double expected, + const double relative_tolerance = 1.0e-12) { + ASSERT_TRUE(std::isfinite(actual)); + ASSERT_TRUE(std::isfinite(expected)); + EXPECT_LE(std::abs(actual - expected), + relative_tolerance * (std::max)(std::abs(expected), 1.0)); } -std::vector makeStationRows( +std::vector MakeStationRows( const RecoveryFixture& fixture) { - const auto& nodes = fixture.domain->Nodes(); - const auto& elements = fixture.domain->Elements(); - return { - {0U, 0, nodes[elements[0U].node_indices[0U]].source_id, - {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {1.0, 2.0, 3.0, 4.0}}, - {0U, 1, nodes[elements[0U].node_indices[1U]].source_id, - {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {5.0, 6.0, 7.0, 8.0}}, - {1U, 0, nodes[elements[1U].node_indices[0U]].source_id, - {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {5.0, 6.0, 7.0, 8.0}}, - {1U, 1, nodes[elements[1U].node_indices[1U]].source_id, - {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, - {9.0, 10.0, 11.0, 12.0}}}; + const auto& nodes = fixture.domain->Nodes(); + const auto& elements = fixture.domain->Elements(); + return {{0U, + 0, + nodes[elements[0U].node_indices[0U]].source_id, + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {1.0, 2.0, 3.0, 4.0}}, + {0U, + 1, + nodes[elements[0U].node_indices[1U]].source_id, + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {5.0, 6.0, 7.0, 8.0}}, + {1U, + 0, + nodes[elements[1U].node_indices[0U]].source_id, + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {5.0, 6.0, 7.0, 8.0}}, + {1U, + 1, + nodes[elements[1U].node_indices[1U]].source_id, + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, + {9.0, 10.0, 11.0, 12.0}}}; } -} // namespace +} // namespace TEST(ResultRecovery, ComputesResidualReactionForNonzeroPrescription) { - const auto fixture = makeFixture(); - auto state = makeAxialEquilibriumState(fixture); - fesa::ShellStateCandidate staleShellEvidence{}; - staleShellEvidence.physicalStrainEnergy = 123.0; - staleShellEvidence.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; - staleShellEvidence.verificationMetrics = {1.0e-11, 2.0e-11, 3.0e-11}; - ASSERT_TRUE(state.commitShellResults({}, staleShellEvidence).IsOk()); + const auto fixture = MakeFixture(); + auto state = MakeAxialEquilibriumState(fixture); + fesa::ShellStateCandidate stale_shell_evidence{}; + stale_shell_evidence.physical_strain_energy = 123.0; + stale_shell_evidence.equilibrium = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}; + stale_shell_evidence.verification_metrics = {1.0e-11, 2.0e-11, 3.0e-11}; + ASSERT_TRUE(state.CommitShellResults({}, stale_shell_evidence).IsOk()); - const fesa::Status status = fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, state); + const fesa::Status status = fesa::ResultRecovery::Recover( + *fixture.model, *fixture.dofs, *fixture.stiffness, state); - ASSERT_TRUE(status.IsOk()); - EXPECT_DOUBLE_EQ(state.internalForce()[0U], -20.0); - EXPECT_DOUBLE_EQ(state.internalForce()[6U], 20.0); - EXPECT_DOUBLE_EQ(state.residual()[0U], -20.0); - EXPECT_DOUBLE_EQ(state.residual()[6U], 0.0); - EXPECT_DOUBLE_EQ(state.reaction()[0U], -20.0); - for (const std::size_t fullDof : fixture.dofs->freeDofs()) { - EXPECT_DOUBLE_EQ(state.reaction()[fullDof], 0.0); - } - EXPECT_TRUE(state.shellResults().empty()); - EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), 0.0); - EXPECT_EQ( - state.equilibrium(), - (std::array{0.0, 0.0, 0.0, 0.0, 0.0, 0.0})); - EXPECT_EQ( - state.verificationMetrics(), - (std::array{0.0, 0.0, 0.0})); + ASSERT_TRUE(status.IsOk()); + EXPECT_DOUBLE_EQ(state.InternalForce()[0U], -20.0); + EXPECT_DOUBLE_EQ(state.InternalForce()[6U], 20.0); + EXPECT_DOUBLE_EQ(state.Residual()[0U], -20.0); + EXPECT_DOUBLE_EQ(state.Residual()[6U], 0.0); + EXPECT_DOUBLE_EQ(state.Reaction()[0U], -20.0); + for (const std::size_t full_dof : fixture.dofs->FreeDofs()) { + EXPECT_DOUBLE_EQ(state.Reaction()[full_dof], 0.0); + } + EXPECT_TRUE(state.ShellResults().empty()); + EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 0.0); + EXPECT_EQ(state.Equilibrium(), + (std::array{0.0, 0.0, 0.0, 0.0, 0.0, 0.0})); + EXPECT_EQ(state.VerificationMetrics(), + (std::array{0.0, 0.0, 0.0})); } TEST(ResultRecovery, EnforcesNormalizedFreeResidual) { - const auto fixture = makeFixture(); + const auto fixture = MakeFixture(); - auto failed = makeAxialEquilibriumState(fixture); - failed.internalForce()[0U] = 91.0; - failed.residual()[0U] = 92.0; - failed.reaction()[0U] = 93.0; - failed.reaction()[6U] = 94.0; - failed.endpointResults().push_back({}); - failed.externalForce()[6U] += 1.0e-7; - expectStatusCode( - fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, failed), - "free-residual-tolerance-failure"); - EXPECT_DOUBLE_EQ(failed.internalForce()[0U], 91.0); - EXPECT_DOUBLE_EQ(failed.residual()[0U], 92.0); - EXPECT_DOUBLE_EQ(failed.reaction()[0U], 93.0); - EXPECT_DOUBLE_EQ(failed.reaction()[6U], 94.0); - EXPECT_EQ(failed.endpointResults().size(), 1U); + auto failed = MakeAxialEquilibriumState(fixture); + failed.InternalForce()[0U] = 91.0; + failed.Residual()[0U] = 92.0; + failed.Reaction()[0U] = 93.0; + failed.Reaction()[6U] = 94.0; + failed.EndpointResults().push_back({}); + failed.ExternalForce()[6U] += 1.0e-7; + ExpectStatusCode(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, failed), + "free-residual-tolerance-failure"); + EXPECT_DOUBLE_EQ(failed.InternalForce()[0U], 91.0); + EXPECT_DOUBLE_EQ(failed.Residual()[0U], 92.0); + EXPECT_DOUBLE_EQ(failed.Reaction()[0U], 93.0); + EXPECT_DOUBLE_EQ(failed.Reaction()[6U], 94.0); + EXPECT_EQ(failed.EndpointResults().size(), 1U); - auto thresholdPass = makeAxialEquilibriumState(fixture); - thresholdPass.externalForce()[6U] += 1.0e-10 * 20.0 * 0.5; - const fesa::Status thresholdStatus = fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - thresholdPass); - ASSERT_TRUE(thresholdStatus.IsOk()); - EXPECT_NE(thresholdPass.residual()[6U], 0.0); - EXPECT_DOUBLE_EQ( - thresholdPass.reaction()[6U], thresholdPass.residual()[6U]); + auto threshold_pass = MakeAxialEquilibriumState(fixture); + threshold_pass.ExternalForce()[6U] += 1.0e-10 * 20.0 * 0.5; + const fesa::Status threshold_status = fesa::ResultRecovery::Recover( + *fixture.model, *fixture.dofs, *fixture.stiffness, threshold_pass); + ASSERT_TRUE(threshold_status.IsOk()); + EXPECT_NE(threshold_pass.Residual()[6U], 0.0); + EXPECT_DOUBLE_EQ(threshold_pass.Reaction()[6U], + threshold_pass.Residual()[6U]); - const auto zeroFixture = makeFixture(false, {}, {}, false, false, false); - auto zeroEquilibrium = fesa::AnalysisState::create( - *zeroFixture.dofs, {"Step-1", 0U}); - EXPECT_TRUE(fesa::ResultRecovery::recover( - *zeroFixture.model, - *zeroFixture.dofs, - *zeroFixture.stiffness, - zeroEquilibrium) - .IsOk()); + const auto zero_fixture = MakeFixture(false, {}, {}, false, false, false); + auto zero_equilibrium = + fesa::AnalysisState::Create(*zero_fixture.dofs, {"Step-1", 0U}); + EXPECT_TRUE( + fesa::ResultRecovery::Recover(*zero_fixture.model, *zero_fixture.dofs, + *zero_fixture.stiffness, zero_equilibrium) + .IsOk()); - auto wrongPrescription = makeAxialEquilibriumState(fixture); - wrongPrescription.displacement()[0U] = 0.0; - expectStatusCode( - fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - wrongPrescription), - "invalid-recovery-state"); + auto wrong_prescription = MakeAxialEquilibriumState(fixture); + wrong_prescription.Displacement()[0U] = 0.0; + ExpectStatusCode( + fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, wrong_prescription), + "invalid-recovery-state"); - auto nonfinite = makeAxialEquilibriumState(fixture); - nonfinite.displacement()[6U] = std::numeric_limits::quiet_NaN(); - expectStatusCode( - fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, nonfinite), - "nonfinite-recovery-value"); + auto nonfinite = MakeAxialEquilibriumState(fixture); + nonfinite.Displacement()[6U] = std::numeric_limits::quiet_NaN(); + ExpectStatusCode(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, nonfinite), + "nonfinite-recovery-value"); - const auto wrongFixture = makeFixture(true); - auto wrongState = fesa::AnalysisState::create( - *wrongFixture.dofs, {"Step-1", 0U}); - expectStatusCode( - fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, wrongState), - "invalid-recovery-dimensions"); + const auto wrong_fixture = MakeFixture(true); + auto wrong_state = + fesa::AnalysisState::Create(*wrong_fixture.dofs, {"Step-1", 0U}); + ExpectStatusCode( + fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, wrong_state), + "invalid-recovery-dimensions"); } TEST(ResultRecovery, KeepsEndActionSectionAndGaussResultsDistinct) { - const auto fixture = makeFixture(); - auto state = makePatchState(fixture, 0.02, 0.03, -0.04, 0.05); + const auto fixture = MakeFixture(); + auto state = MakePatchState(fixture, 0.02, 0.03, -0.04, 0.05); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, state) - .IsOk()); - ASSERT_EQ(state.endpointResults().size(), 2U); - ASSERT_EQ(state.gaussResults().size(), 2U); - EXPECT_EQ(state.endpointResults()[0U].endpoint, 0); - EXPECT_EQ(state.endpointResults()[1U].endpoint, 1); - EXPECT_EQ(state.gaussResults()[0U].gaussPoint, 1); - EXPECT_EQ(state.gaussResults()[1U].gaussPoint, 2); - EXPECT_DOUBLE_EQ( - state.endpointResults()[0U].endAction[0U], - -state.endpointResults()[0U].sectionResultant[0U]); - EXPECT_DOUBLE_EQ( - state.endpointResults()[1U].endAction[0U], - state.endpointResults()[1U].sectionResultant[0U]); - EXPECT_DOUBLE_EQ( - state.gaussResults()[0U].generalizedResultant[0U], - state.endpointResults()[0U].sectionResultant[0U]); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + ASSERT_EQ(state.EndpointResults().size(), 2U); + ASSERT_EQ(state.GaussResults().size(), 2U); + EXPECT_EQ(state.EndpointResults()[0U].endpoint, 0); + EXPECT_EQ(state.EndpointResults()[1U].endpoint, 1); + EXPECT_EQ(state.GaussResults()[0U].gauss_point, 1); + EXPECT_EQ(state.GaussResults()[1U].gauss_point, 2); + EXPECT_DOUBLE_EQ(state.EndpointResults()[0U].end_action[0U], + -state.EndpointResults()[0U].section_resultant[0U]); + EXPECT_DOUBLE_EQ(state.EndpointResults()[1U].end_action[0U], + state.EndpointResults()[1U].section_resultant[0U]); + EXPECT_DOUBLE_EQ(state.GaussResults()[0U].generalized_resultant[0U], + state.EndpointResults()[0U].section_resultant[0U]); } TEST(ResultRecovery, MatchesAxialTorsionAndTwoPlaneEndSigns) { - const auto fixture = makeFixture(); - const double epsilon = 0.02; - const double twist = -0.03; - const double kappaY = 0.04; - const double kappaZ = -0.05; - auto state = makePatchState(fixture, epsilon, twist, kappaY, kappaZ); + const auto fixture = MakeFixture(); + const double epsilon = 0.02; + const double twist = -0.03; + const double kappa_y = 0.04; + const double kappa_z = -0.05; + auto state = MakePatchState(fixture, epsilon, twist, kappa_y, kappa_z); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, state) - .IsOk()); - const double shearModulus = - kYoungsModulus / (2.0 * (1.0 + kPoissonRatio)); - const std::array expected = { - kYoungsModulus * 2.0 * epsilon, - shearModulus * 5.0 * twist, - kYoungsModulus * 3.0 * kappaY, - kYoungsModulus * 4.0 * kappaZ}; - const std::array endComponents = {0U, 3U, 4U, 5U}; - for (std::size_t component = 0U; component < expected.size(); ++component) { - expectScaledNear( - state.endpointResults()[0U].sectionResultant[component], - expected[component]); - expectScaledNear( - state.endpointResults()[1U].sectionResultant[component], - expected[component]); - expectScaledNear( - state.endpointResults()[0U].endAction[endComponents[component]], - -expected[component]); - expectScaledNear( - state.endpointResults()[1U].endAction[endComponents[component]], - expected[component]); - } + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + const double shear_modulus = kYoungsModulus / (2.0 * (1.0 + kPoissonRatio)); + const std::array expected = { + kYoungsModulus * 2.0 * epsilon, shear_modulus * 5.0 * twist, + kYoungsModulus * 3.0 * kappa_y, kYoungsModulus * 4.0 * kappa_z}; + const std::array end_components = {0U, 3U, 4U, 5U}; + for (std::size_t component = 0U; component < expected.size(); ++component) { + ExpectScaledNear(state.EndpointResults()[0U].section_resultant[component], + expected[component]); + ExpectScaledNear(state.EndpointResults()[1U].section_resultant[component], + expected[component]); + ExpectScaledNear( + state.EndpointResults()[0U].end_action[end_components[component]], + -expected[component]); + ExpectScaledNear( + state.EndpointResults()[1U].end_action[end_components[component]], + expected[component]); + } } TEST(ResultRecovery, OrdersStressPointsAndDefaultCentroid) { - const std::vector> sectionPoints = { - {0.25, -0.5}, {-0.4, 0.3}}; - const auto fixture = makeFixture(false, sectionPoints); - auto state = makePatchState(fixture, 0.01, 0.0, 0.02, -0.03); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, state) - .IsOk()); - ASSERT_EQ(state.stressResults().size(), 4U); - for (std::size_t gauss = 0U; gauss < 2U; ++gauss) { - for (std::size_t point = 0U; point < sectionPoints.size(); ++point) { - const auto& row = state.stressResults()[gauss * 2U + point]; - EXPECT_EQ(row.element, 0U); - EXPECT_EQ(row.gaussPoint, static_cast(gauss + 1U)); - EXPECT_EQ(row.sectionPoint, point + 1U); - EXPECT_DOUBLE_EQ(row.x1, sectionPoints[point][0U]); - EXPECT_DOUBLE_EQ(row.x2, sectionPoints[point][1U]); - EXPECT_EQ(row.source, "input"); - expectScaledNear( - row.s11, - kYoungsModulus * - (0.01 + row.x2 * 0.02 - row.x1 * -0.03)); - } + const std::vector> section_points = {{0.25, -0.5}, + {-0.4, 0.3}}; + const auto fixture = MakeFixture(false, section_points); + auto state = MakePatchState(fixture, 0.01, 0.0, 0.02, -0.03); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + ASSERT_EQ(state.StressResults().size(), 4U); + for (std::size_t gauss = 0U; gauss < 2U; ++gauss) { + for (std::size_t point = 0U; point < section_points.size(); ++point) { + const auto& row = state.StressResults()[gauss * 2U + point]; + EXPECT_EQ(row.element, 0U); + EXPECT_EQ(row.gauss_point, static_cast(gauss + 1U)); + EXPECT_EQ(row.section_point, point + 1U); + EXPECT_DOUBLE_EQ(row.x1, section_points[point][0U]); + EXPECT_DOUBLE_EQ(row.x2, section_points[point][1U]); + EXPECT_EQ(row.source, "input"); + ExpectScaledNear( + row.s11, kYoungsModulus * (0.01 + row.x2 * 0.02 - row.x1 * -0.03)); } + } - const auto defaultFixture = makeFixture(); - auto defaultState = makePatchState(defaultFixture, 0.01, 0.0, 0.0, 0.0); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *defaultFixture.model, - *defaultFixture.dofs, - *defaultFixture.stiffness, - defaultState) - .IsOk()); - ASSERT_EQ(defaultState.stressResults().size(), 2U); - for (const auto& row : defaultState.stressResults()) { - EXPECT_EQ(row.sectionPoint, 0U); - EXPECT_DOUBLE_EQ(row.x1, 0.0); - EXPECT_DOUBLE_EQ(row.x2, 0.0); - EXPECT_EQ(row.source, "fesa-default"); - } + const auto default_fixture = MakeFixture(); + auto default_state = MakePatchState(default_fixture, 0.01, 0.0, 0.0, 0.0); + ASSERT_TRUE(fesa::ResultRecovery::Recover( + *default_fixture.model, *default_fixture.dofs, + *default_fixture.stiffness, default_state) + .IsOk()); + ASSERT_EQ(default_state.StressResults().size(), 2U); + for (const auto& row : default_state.StressResults()) { + EXPECT_EQ(row.section_point, 0U); + EXPECT_DOUBLE_EQ(row.x1, 0.0); + EXPECT_DOUBLE_EQ(row.x2, 0.0); + EXPECT_EQ(row.source, "fesa-default"); + } } TEST(ResultRecovery, RequiresInteriorEndpointConsistencyWithoutAveraging) { - const auto fixture = makeFixture(true); - const std::array tolerances = {1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6}; - auto rows = makeStationRows(fixture); - rows[2U].sectionResultant[0U] += 0.5e-6; + const auto fixture = MakeFixture(true); + const std::array tolerances = {1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6}; + auto rows = MakeStationRows(fixture); + rows[2U].section_resultant[0U] += 0.5e-6; - auto normalized = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *fixture.model, rows, tolerances); - ASSERT_TRUE(normalized.HasValue()); - ASSERT_EQ(normalized.Value().size(), 3U); - EXPECT_EQ(normalized.Value()[1U].representativeElement, 0U); - EXPECT_DOUBLE_EQ(normalized.Value()[1U].sectionResultant[0U], 5.0); + auto normalized = + fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *fixture.model, rows, tolerances); + ASSERT_TRUE(normalized.HasValue()); + ASSERT_EQ(normalized.Value().size(), 3U); + EXPECT_EQ(normalized.Value()[1U].representative_element, 0U); + EXPECT_DOUBLE_EQ(normalized.Value()[1U].section_resultant[0U], 5.0); - rows[2U].sectionResultant[0U] = 5.0 + 2.0e-6; - auto mismatch = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *fixture.model, rows, tolerances); - ASSERT_FALSE(mismatch.HasValue()); - expectStatusCode(mismatch.GetStatus(), "node-station-tolerance-failure"); + rows[2U].section_resultant[0U] = 5.0 + 2.0e-6; + auto mismatch = + fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *fixture.model, rows, tolerances); + ASSERT_FALSE(mismatch.HasValue()); + ExpectStatusCode(mismatch.GetStatus(), "node-station-tolerance-failure"); - rows = makeStationRows(fixture); - rows[2U].sectionResultant[1U] = - std::numeric_limits::infinity(); - auto nonfinite = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *fixture.model, rows, tolerances); - ASSERT_FALSE(nonfinite.HasValue()); - expectStatusCode(nonfinite.GetStatus(), "nonfinite-node-station-value"); + rows = MakeStationRows(fixture); + rows[2U].section_resultant[1U] = std::numeric_limits::infinity(); + auto nonfinite = + fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *fixture.model, rows, tolerances); + ASSERT_FALSE(nonfinite.HasValue()); + ExpectStatusCode(nonfinite.GetStatus(), "nonfinite-node-station-value"); - auto invalidTolerance = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *fixture.model, - makeStationRows(fixture), - {1.0e-6, -1.0, 1.0e-6, 1.0e-6}); - ASSERT_FALSE(invalidTolerance.HasValue()); - expectStatusCode( - invalidTolerance.GetStatus(), "invalid-node-station-tolerance"); + auto invalid_tolerance = + fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *fixture.model, MakeStationRows(fixture), + {1.0e-6, -1.0, 1.0e-6, 1.0e-6}); + ASSERT_FALSE(invalid_tolerance.HasValue()); + ExpectStatusCode(invalid_tolerance.GetStatus(), + "invalid-node-station-tolerance"); - const std::filesystem::path source{"models/result-recovery.inp"}; - const auto loadedFixture = makeFixture( - true, {}, {{"2", 2, 1.0, {source, 60U}}}); - auto loaded = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *loadedFixture.model, - makeStationRows(loadedFixture), - tolerances); - ASSERT_FALSE(loaded.HasValue()); - expectStatusCode(loaded.GetStatus(), "ineligible-node-station"); + const std::filesystem::path source{"models/result-recovery.inp"}; + const auto loaded_fixture = + MakeFixture(true, {}, {{"2", 2, 1.0, {source, 60U}}}); + auto loaded = fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *loaded_fixture.model, MakeStationRows(loaded_fixture), tolerances); + ASSERT_FALSE(loaded.HasValue()); + ExpectStatusCode(loaded.GetStatus(), "ineligible-node-station"); - const auto reversedFixture = makeFixture(true, {}, {}, true); - auto reversed = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *reversedFixture.model, - makeStationRows(reversedFixture), - tolerances); - ASSERT_FALSE(reversed.HasValue()); - expectStatusCode(reversed.GetStatus(), "ineligible-node-station"); + const auto reversed_fixture = MakeFixture(true, {}, {}, true); + auto reversed = + fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *reversed_fixture.model, MakeStationRows(reversed_fixture), + tolerances); + ASSERT_FALSE(reversed.HasValue()); + ExpectStatusCode(reversed.GetStatus(), "ineligible-node-station"); - const auto jumpFixture = makeFixture(true, {}, {}, false, true); - auto jumped = - fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( - *jumpFixture.model, - makeStationRows(jumpFixture), - tolerances); - ASSERT_FALSE(jumped.HasValue()); - expectStatusCode(jumped.GetStatus(), "ineligible-node-station"); + const auto jump_fixture = MakeFixture(true, {}, {}, false, true); + auto jumped = fesa::ResultRecovery::NormalizeSectionResultantsToNodeStations( + *jump_fixture.model, MakeStationRows(jump_fixture), tolerances); + ASSERT_FALSE(jumped.HasValue()); + ExpectStatusCode(jumped.GetStatus(), "ineligible-node-station"); } // MITC4-REC-001 TEST(ResultRecovery, RecoversShellRowsInStableElementAndGpOrder) { - const auto fixture = makeShellFixture(makeShellDefinition(true)); - auto state = fesa::AnalysisState::create( - *fixture.dofs, {"Step-1", 0U}); - for (std::size_t node = 0U; - node < fixture.domain->Nodes().size(); - ++node) { - const double x = fixture.domain->Nodes()[node].coordinates[0U]; - const double y = fixture.domain->Nodes()[node].coordinates[1U]; - state.displacement()[node * 6U] = 0.1 * x + 0.1 * y; - state.displacement()[node * 6U + 1U] = -0.05 * y + 0.1 * x; - } - state.externalForce() = - fixture.stiffness->Multiply(state.displacement()); + const auto fixture = MakeShellFixture(MakeShellDefinition(true)); + auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) { + const double x = fixture.domain->Nodes()[node].coordinates[0U]; + const double y = fixture.domain->Nodes()[node].coordinates[1U]; + state.Displacement()[node * 6U] = 0.1 * x + 0.1 * y; + state.Displacement()[node * 6U + 1U] = -0.05 * y + 0.1 * x; + } + state.ExternalForce() = fixture.stiffness->Multiply(state.Displacement()); - const auto status = fesa::ResultRecovery::recover( - *fixture.model, *fixture.dofs, *fixture.stiffness, state); + const auto status = fesa::ResultRecovery::Recover( + *fixture.model, *fixture.dofs, *fixture.stiffness, state); - ASSERT_TRUE(status.IsOk()); - ASSERT_EQ(state.shellResults().size(), 8U); - const double gauss = 1.0 / std::sqrt(3.0); - const std::array locations{ - fesa::ShellMidsurfaceLocation::gp1, - fesa::ShellMidsurfaceLocation::gp2, - fesa::ShellMidsurfaceLocation::gp3, - fesa::ShellMidsurfaceLocation::gp4}; - const std::array, 4> coordinates{ - std::array{-gauss, -gauss}, - std::array{gauss, -gauss}, - std::array{gauss, gauss}, - std::array{-gauss, gauss}}; - constexpr std::array expectedStrain{ - 0.1, -0.05, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0}; - constexpr std::array expectedResultant{ - 22.4, -6.4, 19.2, 0.0, 0.0, 0.0, 0.0, 0.0}; - const std::array, 3> expectedFrame{{ - {1.0, 0.0, 0.0}, - {0.0, 1.0, 0.0}, - {0.0, 0.0, 1.0}}}; - for (std::size_t element = 0U; element < 2U; ++element) { - for (std::size_t point = 0U; point < locations.size(); ++point) { - const auto& row = state.shellResults()[element * 4U + point]; - EXPECT_EQ(row.element, element); - EXPECT_EQ(row.location, locations[point]); - EXPECT_EQ(row.naturalCoordinates, coordinates[point]); - EXPECT_EQ(row.localFrame, expectedFrame); - for (std::size_t component = 0U; - component < expectedStrain.size(); - ++component) { - EXPECT_NEAR( - row.generalizedStrain[component], - expectedStrain[component], - 1.0e-12); - EXPECT_NEAR( - row.sectionResultant[component], - expectedResultant[component], - 1.0e-12); - } - } + ASSERT_TRUE(status.IsOk()); + ASSERT_EQ(state.ShellResults().size(), 8U); + const double gauss = 1.0 / std::sqrt(3.0); + const std::array locations{ + fesa::ShellMidsurfaceLocation::kGp1, fesa::ShellMidsurfaceLocation::kGp2, + fesa::ShellMidsurfaceLocation::kGp3, fesa::ShellMidsurfaceLocation::kGp4}; + const std::array, 4> coordinates{ + std::array{-gauss, -gauss}, + std::array{gauss, -gauss}, std::array{gauss, gauss}, + std::array{-gauss, gauss}}; + constexpr std::array expected_strain{0.1, -0.05, 0.2, 0.0, + 0.0, 0.0, 0.0, 0.0}; + constexpr std::array expected_resultant{22.4, -6.4, 19.2, 0.0, + 0.0, 0.0, 0.0, 0.0}; + const std::array, 3> expected_frame{ + {{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}, {0.0, 0.0, 1.0}}}; + for (std::size_t element = 0U; element < 2U; ++element) { + for (std::size_t point = 0U; point < locations.size(); ++point) { + const auto& row = state.ShellResults()[element * 4U + point]; + EXPECT_EQ(row.element, element); + EXPECT_EQ(row.location, locations[point]); + EXPECT_EQ(row.natural_coordinates, coordinates[point]); + EXPECT_EQ(row.local_frame, expected_frame); + for (std::size_t component = 0U; component < expected_strain.size(); + ++component) { + EXPECT_NEAR(row.generalized_strain[component], + expected_strain[component], 1.0e-12); + EXPECT_NEAR(row.section_resultant[component], + expected_resultant[component], 1.0e-12); + } } + } } // MITC4-REC-002 TEST(ResultRecovery, RecoversDirectBottomMiddleTopShellStress) { - const auto fixture = makeShellFixture(makeShellDefinition()); - auto state = makeShellPhysicalState(fixture); + const auto fixture = MakeShellFixture(MakeShellDefinition()); + auto state = MakeShellPhysicalState(fixture); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - state) - .IsOk()); - constexpr std::array positions{ - fesa::ShellSectionPosition::bottom, - fesa::ShellSectionPosition::middle, - fesa::ShellSectionPosition::top}; - constexpr std::array zeta{-1.0, 0.0, 1.0}; - constexpr std::array, 3> expectedStress{{ - {-22.4, 6.4, -2.4}, - {11.2, -3.2, 9.6}, - {44.8, -12.8, 21.6}}}; - ASSERT_EQ(state.shellResults().size(), 4U); - for (const auto& row : state.shellResults()) { - for (std::size_t position = 0U; - position < positions.size(); - ++position) { - EXPECT_EQ(row.stress[position].position, positions[position]); - EXPECT_DOUBLE_EQ(row.stress[position].zeta, zeta[position]); - for (std::size_t component = 0U; - component < expectedStress[position].size(); - ++component) { - EXPECT_NEAR( - row.stress[position].components[component], - expectedStress[position][component], - 1.0e-12); - } - } + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + constexpr std::array positions{ + fesa::ShellSectionPosition::kBottom, fesa::ShellSectionPosition::kMiddle, + fesa::ShellSectionPosition::kTop}; + constexpr std::array zeta{-1.0, 0.0, 1.0}; + constexpr std::array, 3> expected_stress{ + {{-22.4, 6.4, -2.4}, {11.2, -3.2, 9.6}, {44.8, -12.8, 21.6}}}; + ASSERT_EQ(state.ShellResults().size(), 4U); + for (const auto& row : state.ShellResults()) { + for (std::size_t position = 0U; position < positions.size(); ++position) { + EXPECT_EQ(row.stress[position].position, positions[position]); + EXPECT_DOUBLE_EQ(row.stress[position].zeta, zeta[position]); + for (std::size_t component = 0U; + component < expected_stress[position].size(); ++component) { + EXPECT_NEAR(row.stress[position].components[component], + expected_stress[position][component], 1.0e-12); + } } + } } // MITC4-REC-003 TEST(ResultRecovery, SumsOnlyPhysicalShellEnergyInSourceOrder) { - const auto fixture = makeShellFixture(makeShellDefinition()); - auto state = makeShellPhysicalState(fixture); - constexpr std::array drill{1.0, -1.0, 1.0, -1.0}; - for (std::size_t node = 0U; node < drill.size(); ++node) { - state.displacement()[node * 6U + 5U] = drill[node]; - } - state.externalForce() = - fixture.stiffness->Multiply(state.displacement()); - const double stabilizedEnergy = 0.5 * state.displacement().Dot( - fixture.stiffness->Multiply(state.displacement())); + const auto fixture = MakeShellFixture(MakeShellDefinition()); + auto state = MakeShellPhysicalState(fixture); + constexpr std::array drill{1.0, -1.0, 1.0, -1.0}; + for (std::size_t node = 0U; node < drill.size(); ++node) { + state.Displacement()[node * 6U + 5U] = drill[node]; + } + state.ExternalForce() = fixture.stiffness->Multiply(state.Displacement()); + const double stabilized_energy = + 0.5 * state.Displacement().Dot( + fixture.stiffness->Multiply(state.Displacement())); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - state) - .IsOk()); - EXPECT_NEAR(state.physicalStrainEnergy(), 72.16, 1.0e-12); - EXPECT_GT(stabilizedEnergy, state.physicalStrainEnergy()); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + EXPECT_NEAR(state.PhysicalStrainEnergy(), 72.16, 1.0e-12); + EXPECT_GT(stabilized_energy, state.PhysicalStrainEnergy()); } // MITC4-REC-004 TEST(ResultRecovery, KeepsFullResidualAndComputesGlobalShellEquilibrium) { - const std::filesystem::path source{"models/shell-result-recovery.inp"}; - const std::vector loads{ - {"1", 1, 5.0, {source, 70U}}, - {"5", 1, -5.0, {source, 71U}}, - {"1", 4, 2.0, {source, 72U}}, - {"5", 4, -2.0, {source, 73U}}}; - const auto fixture = makeShellFixture( - makeShellDefinition(false, true, loads)); - auto state = fesa::AnalysisState::create( - *fixture.dofs, {"Step-1", 0U}); - auto fullLoad = fesa::LoadAssembler::assembleFullNodalLoad( - *fixture.model, *fixture.dofs); - ASSERT_TRUE(fullLoad.HasValue()); - state.externalForce() = std::move(fullLoad.Value()); + const std::filesystem::path source{"models/shell-result-recovery.inp"}; + const std::vector loads{{"1", 1, 5.0, {source, 70U}}, + {"5", 1, -5.0, {source, 71U}}, + {"1", 4, 2.0, {source, 72U}}, + {"5", 4, -2.0, {source, 73U}}}; + const auto fixture = + MakeShellFixture(MakeShellDefinition(false, true, loads)); + auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U}); + auto full_load = + fesa::LoadAssembler::AssembleFullNodalLoad(*fixture.model, *fixture.dofs); + ASSERT_TRUE(full_load.HasValue()); + state.ExternalForce() = std::move(full_load.Value()); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - state) - .IsOk()); - ASSERT_EQ(state.shellResults().size(), 4U); - for (std::size_t fullDof = 0U; - fullDof < fixture.dofs->fullDofCount(); - ++fullDof) { - EXPECT_DOUBLE_EQ(state.internalForce()[fullDof], 0.0); - EXPECT_DOUBLE_EQ( - state.residual()[fullDof], -state.externalForce()[fullDof]); - EXPECT_DOUBLE_EQ(state.reaction()[fullDof], state.residual()[fullDof]); - } - EXPECT_EQ( - state.equilibrium(), - (std::array{0.0, 0.0, 0.0, 0.0, 0.0, 0.0})); - EXPECT_EQ( - state.verificationMetrics(), - (std::array{0.0, 0.0, 0.0})); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, state) + .IsOk()); + ASSERT_EQ(state.ShellResults().size(), 4U); + for (std::size_t full_dof = 0U; full_dof < fixture.dofs->FullDofCount(); + ++full_dof) { + EXPECT_DOUBLE_EQ(state.InternalForce()[full_dof], 0.0); + EXPECT_DOUBLE_EQ(state.Residual()[full_dof], + -state.ExternalForce()[full_dof]); + EXPECT_DOUBLE_EQ(state.Reaction()[full_dof], state.Residual()[full_dof]); + } + EXPECT_EQ(state.Equilibrium(), + (std::array{0.0, 0.0, 0.0, 0.0, 0.0, 0.0})); + EXPECT_EQ(state.VerificationMetrics(), + (std::array{0.0, 0.0, 0.0})); - const auto freeFixture = makeShellFixture(makeShellDefinition()); - auto perturbed = makeShellPhysicalState(freeFixture); - perturbed.externalForce()[0U] += 1.0e-9; - ASSERT_TRUE(fesa::ResultRecovery::recover( - *freeFixture.model, - *freeFixture.dofs, - *freeFixture.stiffness, - perturbed) - .IsOk()); - for (const double metric : perturbed.verificationMetrics()) { - EXPECT_GT(metric, 0.0); - EXPECT_LE(metric, 1.0e-10); - } + const auto free_fixture = MakeShellFixture(MakeShellDefinition()); + auto perturbed = MakeShellPhysicalState(free_fixture); + perturbed.ExternalForce()[0U] += 1.0e-9; + ASSERT_TRUE(fesa::ResultRecovery::Recover(*free_fixture.model, + *free_fixture.dofs, + *free_fixture.stiffness, perturbed) + .IsOk()); + for (const double metric : perturbed.VerificationMetrics()) { + EXPECT_GT(metric, 0.0); + EXPECT_LE(metric, 1.0e-10); + } } // MITC4-REC-004 TEST(ResultRecovery, UsesGlobalOriginForShellMomentBalance) { - auto centeredDefinition = makeShellDefinition(); - auto translatedDefinition = centeredDefinition; - constexpr std::array translation{7.0, 11.0, 0.0}; - for (auto& node : translatedDefinition.nodes) { - for (std::size_t component = 0U; - component < translation.size(); - ++component) { - node.coordinates[component] += translation[component]; - } + auto centered_definition = MakeShellDefinition(); + auto translated_definition = centered_definition; + constexpr std::array translation{7.0, 11.0, 0.0}; + for (auto& node : translated_definition.nodes) { + for (std::size_t component = 0U; component < translation.size(); + ++component) { + node.coordinates[component] += translation[component]; } - const auto centeredFixture = makeShellFixture( - std::move(centeredDefinition)); - const auto translatedFixture = makeShellFixture( - std::move(translatedDefinition)); - auto centered = makeShellPhysicalState(centeredFixture); - auto translated = makeShellPhysicalState(translatedFixture); - centered.externalForce()[0U] += 1.0e-9; - translated.externalForce()[0U] += 1.0e-9; + } + const auto centered_fixture = + MakeShellFixture(std::move(centered_definition)); + const auto translated_fixture = + MakeShellFixture(std::move(translated_definition)); + auto centered = MakeShellPhysicalState(centered_fixture); + auto translated = MakeShellPhysicalState(translated_fixture); + centered.ExternalForce()[0U] += 1.0e-9; + translated.ExternalForce()[0U] += 1.0e-9; - ASSERT_TRUE(fesa::ResultRecovery::recover( - *centeredFixture.model, - *centeredFixture.dofs, - *centeredFixture.stiffness, - centered) - .IsOk()); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *translatedFixture.model, - *translatedFixture.dofs, - *translatedFixture.stiffness, - translated) - .IsOk()); - std::array centeredForce{}; - for (std::size_t component = 0U; component < 3U; ++component) { - centeredForce[component] = centered.equilibrium()[component]; - EXPECT_NEAR( - translated.equilibrium()[component], - centeredForce[component], - 1.0e-12); - } - const std::array translatedMomentDelta{ - translation[1U] * centeredForce[2U] - - translation[2U] * centeredForce[1U], - translation[2U] * centeredForce[0U] - - translation[0U] * centeredForce[2U], - translation[0U] * centeredForce[1U] - - translation[1U] * centeredForce[0U]}; - for (std::size_t component = 0U; component < 3U; ++component) { - EXPECT_NEAR( - translated.equilibrium()[3U + component] - - centered.equilibrium()[3U + component], - translatedMomentDelta[component], - 5.0e-12); - } - EXPECT_GT(std::abs(translatedMomentDelta[2U]), 1.0e-9); + ASSERT_TRUE(fesa::ResultRecovery::Recover( + *centered_fixture.model, *centered_fixture.dofs, + *centered_fixture.stiffness, centered) + .IsOk()); + ASSERT_TRUE(fesa::ResultRecovery::Recover( + *translated_fixture.model, *translated_fixture.dofs, + *translated_fixture.stiffness, translated) + .IsOk()); + std::array centered_force{}; + for (std::size_t component = 0U; component < 3U; ++component) { + centered_force[component] = centered.Equilibrium()[component]; + EXPECT_NEAR(translated.Equilibrium()[component], centered_force[component], + 1.0e-12); + } + const std::array translated_moment_delta{ + translation[1U] * centered_force[2U] - + translation[2U] * centered_force[1U], + translation[2U] * centered_force[0U] - + translation[0U] * centered_force[2U], + translation[0U] * centered_force[1U] - + translation[1U] * centered_force[0U]}; + for (std::size_t component = 0U; component < 3U; ++component) { + EXPECT_NEAR(translated.Equilibrium()[3U + component] - + centered.Equilibrium()[3U + component], + translated_moment_delta[component], 5.0e-12); + } + EXPECT_GT(std::abs(translated_moment_delta[2U]), 1.0e-9); } // MITC4-REC-004 TEST(ResultRecovery, UsesScaleAwareShellMetricsAndRejectsExcess) { - const auto fixture = makeShellFixture(makeShellDefinition()); - auto subunit = makeShellPhysicalState(fixture); - auto large = makeShellPhysicalState(fixture); - constexpr double subunitScale = 1.0e-6; - constexpr double largeScale = 1.0e6; - subunit.displacement().Scale(subunitScale); - subunit.externalForce().Scale(subunitScale); - subunit.externalForce()[0U] += 1.0e-9 * subunitScale; - large.displacement().Scale(largeScale); - large.externalForce().Scale(largeScale); - large.externalForce()[0U] += 1.0e-9 * largeScale; + const auto fixture = MakeShellFixture(MakeShellDefinition()); + auto subunit = MakeShellPhysicalState(fixture); + auto large = MakeShellPhysicalState(fixture); + constexpr double subunit_scale = 1.0e-6; + constexpr double large_scale = 1.0e6; + subunit.Displacement().Scale(subunit_scale); + subunit.ExternalForce().Scale(subunit_scale); + subunit.ExternalForce()[0U] += 1.0e-9 * subunit_scale; + large.Displacement().Scale(large_scale); + large.ExternalForce().Scale(large_scale); + large.ExternalForce()[0U] += 1.0e-9 * large_scale; - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - subunit) - .IsOk()); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *fixture.model, - *fixture.dofs, - *fixture.stiffness, - large) - .IsOk()); - for (std::size_t metric = 0U; metric < 3U; ++metric) { - EXPECT_GT(subunit.verificationMetrics()[metric], 0.0); - EXPECT_GT(large.verificationMetrics()[metric], 0.0); - EXPECT_NEAR( - subunit.verificationMetrics()[metric], - large.verificationMetrics()[metric], - 1.0e-13); - } + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, subunit) + .IsOk()); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs, + *fixture.stiffness, large) + .IsOk()); + for (std::size_t metric = 0U; metric < 3U; ++metric) { + EXPECT_GT(subunit.VerificationMetrics()[metric], 0.0); + EXPECT_GT(large.VerificationMetrics()[metric], 0.0); + EXPECT_NEAR(subunit.VerificationMetrics()[metric], + large.VerificationMetrics()[metric], 1.0e-13); + } - auto constrainedDefinition = makeShellDefinition(false, true); - constrainedDefinition.steps[0U].boundaries[0U].value = 1.0; - const auto constrainedFixture = makeShellFixture( - std::move(constrainedDefinition)); - auto rejected = fesa::AnalysisState::create( - *constrainedFixture.dofs, {"Step-1", 0U}); - for (std::size_t fullDof = 0U; - fullDof < constrainedFixture.dofs->fullDofCount(); - ++fullDof) { - rejected.displacement()[fullDof] = 1.0; - } - const std::vector unbalancedEntry{ - {0U, 0U, 1.0, 0U, 0U}}; - auto unbalancedStiffness = fesa::SparseMatrix::FromCoo( - constrainedFixture.dofs->fullDofCount(), - constrainedFixture.dofs->fullDofCount(), - unbalancedEntry, - constrainedFixture.dofs->sparsePattern()); - ASSERT_TRUE(unbalancedStiffness.HasValue()); - expectStatusCode( - fesa::ResultRecovery::recover( - *constrainedFixture.model, - *constrainedFixture.dofs, - unbalancedStiffness.Value(), - rejected), - "global-equilibrium-tolerance-failure"); + auto constrained_definition = MakeShellDefinition(false, true); + constrained_definition.steps[0U].boundaries[0U].value = 1.0; + const auto constrained_fixture = + MakeShellFixture(std::move(constrained_definition)); + auto rejected = + fesa::AnalysisState::Create(*constrained_fixture.dofs, {"Step-1", 0U}); + for (std::size_t full_dof = 0U; + full_dof < constrained_fixture.dofs->FullDofCount(); ++full_dof) { + rejected.Displacement()[full_dof] = 1.0; + } + const std::vector unbalanced_entry{ + {0U, 0U, 1.0, 0U, 0U}}; + auto unbalanced_stiffness = fesa::SparseMatrix::FromCoo( + constrained_fixture.dofs->FullDofCount(), + constrained_fixture.dofs->FullDofCount(), unbalanced_entry, + constrained_fixture.dofs->GetSparsePattern()); + ASSERT_TRUE(unbalanced_stiffness.HasValue()); + ExpectStatusCode(fesa::ResultRecovery::Recover( + *constrained_fixture.model, *constrained_fixture.dofs, + unbalanced_stiffness.Value(), rejected), + "global-equilibrium-tolerance-failure"); } // MITC4-REC-005 TEST(ResultRecovery, InvalidLaterShellLeavesEntirePriorStateUnchanged) { - const auto validFixture = makeShellFixture(makeShellDefinition(true)); - auto state = makeShellPhysicalState(validFixture); - ASSERT_TRUE(fesa::ResultRecovery::recover( - *validFixture.model, - *validFixture.dofs, - *validFixture.stiffness, - state) - .IsOk()); - ASSERT_EQ(state.shellResults().size(), 8U); - const auto priorFirstRow = state.shellResults().front(); - const double priorEnergy = state.physicalStrainEnergy(); - const auto priorEquilibrium = state.equilibrium(); - const auto priorMetrics = state.verificationMetrics(); - state.internalForce()[0U] = 91.0; - state.residual()[0U] = 92.0; - state.reaction()[0U] = 93.0; - state.endpointResults().push_back({}); - state.displacement()[2U * 6U] = - (std::numeric_limits::max)(); - state.displacement()[5U * 6U] = - (std::numeric_limits::max)(); - state.externalForce() = fesa::Vector{validFixture.dofs->fullDofCount()}; - auto zeroStiffness = fesa::SparseMatrix::FromCoo( - validFixture.dofs->fullDofCount(), - validFixture.dofs->fullDofCount(), - {}, - validFixture.dofs->sparsePattern()); - ASSERT_TRUE(zeroStiffness.HasValue()); + const auto valid_fixture = MakeShellFixture(MakeShellDefinition(true)); + auto state = MakeShellPhysicalState(valid_fixture); + ASSERT_TRUE(fesa::ResultRecovery::Recover(*valid_fixture.model, + *valid_fixture.dofs, + *valid_fixture.stiffness, state) + .IsOk()); + ASSERT_EQ(state.ShellResults().size(), 8U); + const auto prior_first_row = state.ShellResults().front(); + const double prior_energy = state.PhysicalStrainEnergy(); + const auto prior_equilibrium = state.Equilibrium(); + const auto prior_metrics = state.VerificationMetrics(); + state.InternalForce()[0U] = 91.0; + state.Residual()[0U] = 92.0; + state.Reaction()[0U] = 93.0; + state.EndpointResults().push_back({}); + state.Displacement()[2U * 6U] = (std::numeric_limits::max)(); + state.Displacement()[5U * 6U] = (std::numeric_limits::max)(); + state.ExternalForce() = fesa::Vector{valid_fixture.dofs->FullDofCount()}; + auto zero_stiffness = fesa::SparseMatrix::FromCoo( + valid_fixture.dofs->FullDofCount(), valid_fixture.dofs->FullDofCount(), + {}, valid_fixture.dofs->GetSparsePattern()); + ASSERT_TRUE(zero_stiffness.HasValue()); - const auto status = fesa::ResultRecovery::recover( - *validFixture.model, - *validFixture.dofs, - zeroStiffness.Value(), - state); + const auto status = fesa::ResultRecovery::Recover( + *valid_fixture.model, *valid_fixture.dofs, zero_stiffness.Value(), state); - expectStatusCode(status, "invalid-shell-recovery"); - EXPECT_DOUBLE_EQ(state.internalForce()[0U], 91.0); - EXPECT_DOUBLE_EQ(state.residual()[0U], 92.0); - EXPECT_DOUBLE_EQ(state.reaction()[0U], 93.0); - EXPECT_EQ(state.endpointResults().size(), 1U); - ASSERT_EQ(state.shellResults().size(), 8U); - EXPECT_EQ(state.shellResults().front().element, priorFirstRow.element); - EXPECT_EQ(state.shellResults().front().location, priorFirstRow.location); - EXPECT_EQ( - state.shellResults().front().generalizedStrain, - priorFirstRow.generalizedStrain); - EXPECT_EQ( - state.shellResults().front().sectionResultant, - priorFirstRow.sectionResultant); - EXPECT_DOUBLE_EQ(state.physicalStrainEnergy(), priorEnergy); - EXPECT_EQ(state.equilibrium(), priorEquilibrium); - EXPECT_EQ(state.verificationMetrics(), priorMetrics); + ExpectStatusCode(status, "invalid-shell-recovery"); + EXPECT_DOUBLE_EQ(state.InternalForce()[0U], 91.0); + EXPECT_DOUBLE_EQ(state.Residual()[0U], 92.0); + EXPECT_DOUBLE_EQ(state.Reaction()[0U], 93.0); + EXPECT_EQ(state.EndpointResults().size(), 1U); + ASSERT_EQ(state.ShellResults().size(), 8U); + EXPECT_EQ(state.ShellResults().front().element, prior_first_row.element); + EXPECT_EQ(state.ShellResults().front().location, prior_first_row.location); + EXPECT_EQ(state.ShellResults().front().generalized_strain, + prior_first_row.generalized_strain); + EXPECT_EQ(state.ShellResults().front().section_resultant, + prior_first_row.section_resultant); + EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), prior_energy); + EXPECT_EQ(state.Equilibrium(), prior_equilibrium); + EXPECT_EQ(state.VerificationMetrics(), prior_metrics); } diff --git a/tests/unit/results/results_writer_test.cpp b/tests/unit/results/results_writer_test.cpp index ffb3e13..b2dceb8 100644 --- a/tests/unit/results/results_writer_test.cpp +++ b/tests/unit/results/results_writer_test.cpp @@ -1,24 +1,23 @@ -#include "fesa/results/results_writer.hpp" - -#include "fesa/analysis/analysis_state.hpp" -#include "fesa/core/diagnostic.h" -#include "fesa/core/status.h" -#include "fesa/model/domain.h" +#include "fesa/results/results_writer.h" #include #include #include +#include "fesa/analysis/analysis_state.h" +#include "fesa/core/diagnostic.h" +#include "fesa/core/status.h" +#include "fesa/model/domain.h" + namespace { using WriteSignature = fesa::Status (fesa::ResultsWriter::*)( - const std::filesystem::path&, - const fesa::Domain&, - const fesa::AnalysisState&, - const std::vector&); + const std::filesystem::path&, const fesa::Domain&, + const fesa::AnalysisState&, const std::vector&); static_assert(std::has_virtual_destructor_v); static_assert(std::is_abstract_v); -static_assert(std::is_same_v); +static_assert( + std::is_same_v); -} // namespace +} // namespace diff --git a/tests/unit/solvers/linear/linear_solver_test.cpp b/tests/unit/solvers/linear/linear_solver_test.cpp index 30daffa..b9b95aa 100644 --- a/tests/unit/solvers/linear/linear_solver_test.cpp +++ b/tests/unit/solvers/linear/linear_solver_test.cpp @@ -6,7 +6,7 @@ #include #include -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" @@ -19,15 +19,15 @@ fesa::SparseMatrix MakeDenseCsr(const std::size_t rows, fesa::SparsePattern pattern; std::vector contributions; - pattern.rowOffsets.reserve(rows + 1U); - pattern.rowOffsets.push_back(0U); + pattern.row_offsets.reserve(rows + 1U); + pattern.row_offsets.push_back(0U); for (std::size_t row = 0U; row < rows; ++row) { for (std::size_t column = 0U; column < columns; ++column) { - pattern.columnIndices.push_back(column); + pattern.column_indices.push_back(column); contributions.push_back( {row, column, values[row * columns + column], row, column}); } - pattern.rowOffsets.push_back(pattern.columnIndices.size()); + pattern.row_offsets.push_back(pattern.column_indices.size()); } auto matrix = fesa::SparseMatrix::FromCoo(rows, columns, diff --git a/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp b/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp index 8d9b769..fd5f608 100644 --- a/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp +++ b/tests/unit/solvers/linear/mkl_pardiso_solver_test.cpp @@ -9,7 +9,7 @@ #include #include -#include "fesa/fem/dof_manager.hpp" +#include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" namespace { @@ -20,15 +20,15 @@ fesa::SparseMatrix MakeDenseCsr(const std::size_t size, fesa::SparsePattern pattern; std::vector contributions; - pattern.rowOffsets.reserve(size + 1U); - pattern.rowOffsets.push_back(0U); + pattern.row_offsets.reserve(size + 1U); + pattern.row_offsets.push_back(0U); for (std::size_t row = 0U; row < size; ++row) { for (std::size_t column = 0U; column < size; ++column) { - pattern.columnIndices.push_back(column); + pattern.column_indices.push_back(column); contributions.push_back( {row, column, values[row * size + column], row, column}); } - pattern.rowOffsets.push_back(pattern.columnIndices.size()); + pattern.row_offsets.push_back(pattern.column_indices.size()); } auto matrix = fesa::SparseMatrix::FromCoo(size, size,