From 64a43948ef07ba1006c91d7d422406377d9e5519 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sun, 16 Aug 2026 11:23:20 +0900 Subject: [PATCH] feat(cpp-object-oriented-modular-refactoring): step 19 - boundary-condition-policy --- .../fesa/analysis/linear_static_analysis.h | 2 +- include/fesa/constraints/boundary_condition.h | 71 ++++++ .../constraints/essential_constraint_policy.h | 43 ++++ .../fesa/constraints/essential_constraints.h | 41 ---- .../constraints/prescribed_displacement.h | 48 ++++ include/fesa/fem/dof_manager.h | 10 +- include/fesa/model/domain.h | 15 +- include/fesa/model/model_types.h | 4 +- src/fesa/CMakeLists.txt | 3 +- src/fesa/analysis/analysis_model.cpp | 3 +- src/fesa/analysis/linear_static_analysis.cpp | 9 +- src/fesa/assembly/load_assembler.cpp | 4 +- .../essential_constraint_policy.cpp | 169 ++++++++++++++ .../constraints/essential_constraints.cpp | 161 ------------- .../constraints/prescribed_displacement.cpp | 107 +++++++++ src/fesa/fem/dof_manager.cpp | 121 ++++++---- src/fesa/io/abaqus/domain_mapper.cpp | 12 +- src/fesa/model/domain.cpp | 23 +- tests/CMakeLists.txt | 1 + tests/unit/analysis/analysis_model_test.cpp | 2 +- tests/unit/assembly/load_assembler_test.cpp | 13 +- .../constraints/boundary_condition_test.cpp | 217 ++++++++++++++++++ .../essential_constraints_test.cpp | 149 ++++++------ tests/unit/io/abaqus/domain_mapper_test.cpp | 38 ++- tests/unit/model/domain_test.cpp | 2 +- tests/unit/model/model_types_test.cpp | 2 +- tests/unit/results/result_recovery_test.cpp | 14 +- 27 files changed, 919 insertions(+), 365 deletions(-) create mode 100644 include/fesa/constraints/boundary_condition.h create mode 100644 include/fesa/constraints/essential_constraint_policy.h delete mode 100644 include/fesa/constraints/essential_constraints.h create mode 100644 include/fesa/constraints/prescribed_displacement.h create mode 100644 src/fesa/constraints/essential_constraint_policy.cpp delete mode 100644 src/fesa/constraints/essential_constraints.cpp create mode 100644 src/fesa/constraints/prescribed_displacement.cpp create mode 100644 tests/unit/constraints/boundary_condition_test.cpp diff --git a/include/fesa/analysis/linear_static_analysis.h b/include/fesa/analysis/linear_static_analysis.h index c0c1fba..a915d06 100644 --- a/include/fesa/analysis/linear_static_analysis.h +++ b/include/fesa/analysis/linear_static_analysis.h @@ -7,7 +7,7 @@ #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/analysis_state.h" -#include "fesa/constraints/essential_constraints.h" +#include "fesa/constraints/essential_constraint_policy.h" #include "fesa/core/status.h" #include "fesa/fem/dof_manager.h" #include "fesa/math/sparse_matrix.h" diff --git a/include/fesa/constraints/boundary_condition.h b/include/fesa/constraints/boundary_condition.h new file mode 100644 index 0000000..5605f21 --- /dev/null +++ b/include/fesa/constraints/boundary_condition.h @@ -0,0 +1,71 @@ +#ifndef FESA_CONSTRAINTS_BOUNDARY_CONDITION_H_ +#define FESA_CONSTRAINTS_BOUNDARY_CONDITION_H_ + +#include +#include +#include +#include +#include + +#include "fesa/core/diagnostic.h" +#include "fesa/core/status.h" + +namespace fesa { + +class DofManager; +class Domain; +class SourceTargetResolver; + +/// @brief Describes one prescribed value in stable full-DOF order. +struct ConstraintDefinition { + std::size_t source_order; + std::size_t full_dof_index; + double prescribed_value; +}; + +/// @brief Provides immutable semantic and equation context to a boundary. +/// @note Every referenced object must outlive a constraint request. +struct BoundaryConditionContext { + const Domain& domain; + const DofManager& dof_manager; + const SourceTargetResolver& target_resolver; +}; + +/// @brief Produces ordered constraint definitions without equation mutation. +class BoundaryCondition { + public: + virtual ~BoundaryCondition() = default; + + /// @brief Resolves finite full-DOF definitions in stable target order. + /// @param context Non-owning semantic and equation context for this call. + /// @return Ordered definitions or a structured model failure. + virtual Result> ResolveConstraints( + const BoundaryConditionContext& context) const = 0; + + /// @brief Returns the source location used by policy diagnostics. + const SourceLocation& Location() const noexcept { return location_; } + + /// @brief Returns the source target identity used by policy diagnostics. + const std::string& TargetIdentity() const noexcept { + return target_identity_; + } + + protected: + /// @brief Creates a boundary with optional shared diagnostic provenance. + BoundaryCondition(std::string target_identity = {}, + SourceLocation location = {}) + : target_identity_{std::move(target_identity)}, + location_{std::move(location)} {} + + private: + std::string target_identity_; + SourceLocation location_; +}; + +/// @brief Holds non-owning boundaries in an explicitly supplied source order. +using BoundaryConditionView = + std::vector>; + +} // namespace fesa + +#endif // FESA_CONSTRAINTS_BOUNDARY_CONDITION_H_ diff --git a/include/fesa/constraints/essential_constraint_policy.h b/include/fesa/constraints/essential_constraint_policy.h new file mode 100644 index 0000000..a947f50 --- /dev/null +++ b/include/fesa/constraints/essential_constraint_policy.h @@ -0,0 +1,43 @@ +#ifndef FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINT_POLICY_H_ +#define FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINT_POLICY_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 k_ff; + SparseMatrix k_fc; + SparseMatrix k_cf; + SparseMatrix k_cc; +}; + +/// @brief Applies stable prescribed-displacement elimination. +class EssentialConstraintPolicy { + public: + /// @brief Partitions full stiffness into Kff, Kfc, Kcf, and Kcc. + Result Partition(const SparseMatrix& full_stiffness, + const DofManager& dof_manager) const; + + /// @brief Gathers a full vector in stable free-equation order. + Vector GatherFree(const Vector& full_values, + const DofManager& dof_manager) const; + + /// @brief Gathers a full vector in stable constrained-DOF order. + Vector GatherConstrained(const Vector& full_values, + const DofManager& dof_manager) const; + + /// @brief Reconstructs full d from stable df and exact prescribed dc. + Vector ReconstructFull(const Vector& free_values, + const Vector& constrained_values, + const DofManager& dof_manager) const; +}; + +} // namespace fesa + +#endif // FESA_CONSTRAINTS_ESSENTIAL_CONSTRAINT_POLICY_H_ diff --git a/include/fesa/constraints/essential_constraints.h b/include/fesa/constraints/essential_constraints.h deleted file mode 100644 index 24d06c7..0000000 --- a/include/fesa/constraints/essential_constraints.h +++ /dev/null @@ -1,41 +0,0 @@ -#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/prescribed_displacement.h b/include/fesa/constraints/prescribed_displacement.h new file mode 100644 index 0000000..e8995ce --- /dev/null +++ b/include/fesa/constraints/prescribed_displacement.h @@ -0,0 +1,48 @@ +#ifndef FESA_CONSTRAINTS_PRESCRIBED_DISPLACEMENT_H_ +#define FESA_CONSTRAINTS_PRESCRIBED_DISPLACEMENT_H_ + +#include + +#include "fesa/constraints/boundary_condition.h" +#include "fesa/core/diagnostic.h" +#include "fesa/elements/element.h" +#include "fesa/model/source_target_resolver.h" + +namespace fesa { + +/// @brief Emits one prescribed nodal displacement component for a target. +class PrescribedDisplacementBoundaryCondition final : public BoundaryCondition { + public: + /// @brief Creates one prescribed component in stable source order. + PrescribedDisplacementBoundaryCondition(SourceTargetQuery target, + DofComponent component, + double prescribed_value, + std::size_t source_order, + SourceLocation location = {}); + + /// @brief Resolves target-major full-DOF constraint definitions. + Result> ResolveConstraints( + const BoundaryConditionContext& context) const override; + + /// @brief Returns the immutable source target query. + const SourceTargetQuery& Target() const noexcept; + + /// @brief Returns the prescribed global DOF component. + DofComponent Component() const noexcept; + + /// @brief Returns the exact prescribed displacement value. + double PrescribedValue() const noexcept; + + /// @brief Returns the stable boundary-definition order. + std::size_t SourceOrder() const noexcept; + + private: + SourceTargetQuery target_; + DofComponent component_; + double prescribed_value_; + std::size_t source_order_; +}; + +} // namespace fesa + +#endif // FESA_CONSTRAINTS_PRESCRIBED_DISPLACEMENT_H_ diff --git a/include/fesa/fem/dof_manager.h b/include/fesa/fem/dof_manager.h index 07a3902..6fc6ebf 100644 --- a/include/fesa/fem/dof_manager.h +++ b/include/fesa/fem/dof_manager.h @@ -7,6 +7,7 @@ #include #include "fesa/analysis/analysis_model.h" +#include "fesa/constraints/boundary_condition.h" #include "fesa/elements/element.h" #include "fesa/math/vector.h" @@ -39,6 +40,12 @@ class DofManager { Status Build(const AnalysisModel& analysis_model, const ElementView& elements); + /// @brief Builds mappings from explicit runtime elements and boundaries. + /// @param boundaries Non-owning definitions in stable source order. + /// @return Success after atomic replacement or a structured failure. + Status Build(const AnalysisModel& analysis_model, const ElementView& elements, + const BoundaryConditionView& boundaries); + /// @brief Returns the full node-by-component DOF count. std::size_t FullDofCount() const noexcept; /// @brief Returns the free-equation count. @@ -75,7 +82,8 @@ class DofManager { /// @brief Builds from copied layouts after the caller fixes their order. Status BuildLayouts(const AnalysisModel& analysis_model, - const std::vector& layouts); + const std::vector& layouts, + const BoundaryConditionView& boundaries); /// @brief Takes ownership of fully validated stable equation mappings. DofManager(std::size_t full_dof_count, diff --git a/include/fesa/model/domain.h b/include/fesa/model/domain.h index 9932694..d86ce24 100644 --- a/include/fesa/model/domain.h +++ b/include/fesa/model/domain.h @@ -7,6 +7,8 @@ #include #include +#include "fesa/constraints/boundary_condition.h" +#include "fesa/constraints/prescribed_displacement.h" #include "fesa/core/status.h" #include "fesa/loads/concentrated_nodal_load.h" #include "fesa/loads/load.h" @@ -58,8 +60,12 @@ class StepDefinition { /// @brief Returns the source step name. const std::string& Name() const noexcept; - /// @brief Returns current boundary records in declaration order. - const std::vector& Boundaries() const noexcept; + /// @brief Returns polymorphic boundaries in stable source/component order. + const BoundaryConditionView& BoundaryConditions() const noexcept; + + /// @brief Returns prescribed displacements in stable source/component order. + const DomainCollectionView& + PrescribedDisplacements() const noexcept; /// @brief Returns polymorphic loads in stable source order. const LoadView& Loads() const noexcept; @@ -90,7 +96,10 @@ class StepDefinition { explicit StepDefinition(StaticStepDefinition definition); std::string name_; - std::vector boundaries_; + std::vector> boundary_conditions_; + BoundaryConditionView boundary_conditions_view_; + DomainCollectionView + prescribed_displacements_view_; std::vector> loads_; LoadView loads_view_; DomainCollectionView concentrated_loads_view_; diff --git a/include/fesa/model/model_types.h b/include/fesa/model/model_types.h index d947a0b..fc8bc22 100644 --- a/include/fesa/model/model_types.h +++ b/include/fesa/model/model_types.h @@ -36,7 +36,7 @@ struct ShellNodeInitialFrame { }; /// @brief Stores one prescribed nodal degree-of-freedom range. -struct BoundaryCondition { +struct PrescribedDisplacementDefinition { std::string target; int first_dof; int last_dof; @@ -55,7 +55,7 @@ struct NodalLoad { /// @brief Stores the approved single linear-static step definition. struct StaticStepDefinition { std::string name; - std::vector boundaries; + std::vector boundaries; std::vector loads; double initial_increment; double time_period; diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index c503082..8ac22af 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -9,7 +9,8 @@ add_library( assembly/parallel_for.cpp assembly/sparse_assembler.cpp build_info.cpp - constraints/essential_constraints.cpp + constraints/essential_constraint_policy.cpp + constraints/prescribed_displacement.cpp core/ascii.cpp core/diagnostic.cpp core/status.cpp diff --git a/src/fesa/analysis/analysis_model.cpp b/src/fesa/analysis/analysis_model.cpp index 5d9b71f..f6a5baa 100644 --- a/src/fesa/analysis/analysis_model.cpp +++ b/src/fesa/analysis/analysis_model.cpp @@ -101,7 +101,8 @@ AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} { } } - for (std::size_t index = 0U; index < Step().Boundaries().size(); ++index) { + for (std::size_t index = 0U; index < Step().BoundaryConditions().size(); + ++index) { active_boundary_conditions_.push_back(static_cast(index)); } for (std::size_t index = 0U; index < Step().Loads().size(); ++index) { diff --git a/src/fesa/analysis/linear_static_analysis.cpp b/src/fesa/analysis/linear_static_analysis.cpp index be1401f..6d0c0c1 100644 --- a/src/fesa/analysis/linear_static_analysis.cpp +++ b/src/fesa/analysis/linear_static_analysis.cpp @@ -109,7 +109,8 @@ Status LinearStaticAnalysis::AssembleAndPartitionStiffness() { full_stiffness_ = std::make_unique(std::move(stiffness.Value())); - auto partitioned = EssentialConstraints::Partition(*full_stiffness_, *dofs_); + auto partitioned = + EssentialConstraintPolicy{}.Partition(*full_stiffness_, *dofs_); if (!partitioned.HasValue()) { return partitioned.GetStatus(); } @@ -120,7 +121,7 @@ Status LinearStaticAnalysis::AssembleAndPartitionStiffness() { Status LinearStaticAnalysis::Factorize() { // This call intentionally precedes all load assembly in Analysis::Run. - return linear_solver_.Factorize(partitioned_stiffness_->kff); + return linear_solver_.Factorize(partitioned_stiffness_->k_ff); } Status LinearStaticAnalysis::AssembleLoadsAndEffectiveRhs() { @@ -131,7 +132,7 @@ Status LinearStaticAnalysis::AssembleLoadsAndEffectiveRhs() { state_->ExternalForce() = std::move(full_load.Value()); auto rhs = LoadAssembler::EffectiveFreeRhs(state_->ExternalForce(), - partitioned_stiffness_->kfc, + partitioned_stiffness_->k_fc, dofs_->PrescribedValues(), *dofs_); if (!rhs.HasValue()) { return rhs.GetStatus(); @@ -148,7 +149,7 @@ Status LinearStaticAnalysis::SubstituteAndReconstruct() { return solve_status; } - state_->Displacement() = EssentialConstraints::ReconstructFull( + state_->Displacement() = EssentialConstraintPolicy{}.ReconstructFull( free_displacement, dofs_->PrescribedValues(), *dofs_); return Status::Ok(); } diff --git a/src/fesa/assembly/load_assembler.cpp b/src/fesa/assembly/load_assembler.cpp index db7d24c..6b945d2 100644 --- a/src/fesa/assembly/load_assembler.cpp +++ b/src/fesa/assembly/load_assembler.cpp @@ -8,7 +8,7 @@ #include #include -#include "fesa/constraints/essential_constraints.h" +#include "fesa/constraints/essential_constraint_policy.h" #include "fesa/loads/load.h" #include "fesa/model/source_target_resolver.h" @@ -292,7 +292,7 @@ Result LoadAssembler::EffectiveFreeRhs(const Vector& full_load, correction[row] = sum; } - Vector rhs = EssentialConstraints::GatherFree(full_load, dofs); + Vector rhs = EssentialConstraintPolicy{}.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) { diff --git a/src/fesa/constraints/essential_constraint_policy.cpp b/src/fesa/constraints/essential_constraint_policy.cpp new file mode 100644 index 0000000..cf3487d --- /dev/null +++ b/src/fesa/constraints/essential_constraint_policy.cpp @@ -0,0 +1,169 @@ +#include "fesa/constraints/essential_constraint_policy.h" + +#include +#include +#include +#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_CONSTRAINT_POLICY", + identity, + message}}); +} + +/// @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.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}); + } + 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& dof_manager) { + if (!dof_manager.ValidateInvariants().IsOk()) { + throw std::invalid_argument{ + "DofManager constraint dimensions or order are invalid."}; + } +} + +} // namespace + +Result EssentialConstraintPolicy::Partition( + const SparseMatrix& full_stiffness, const DofManager& dof_manager) const { + const Status matrix_status = full_stiffness.Validate(); + if (!matrix_status.IsOk()) { + return Result::Failure(matrix_status); + } + if (full_stiffness.Rows() != full_stiffness.Columns() || + full_stiffness.Rows() != dof_manager.FullDofCount()) { + return Result::Failure(ConstraintFailure( + "invalid-constraint-dimensions", + std::to_string(full_stiffness.Rows()) + "x" + + std::to_string(full_stiffness.Columns()), + "Full stiffness must be square and match the DofManager full " + "dimension.")); + } + const Status dof_status = dof_manager.ValidateInvariants(); + if (!dof_status.IsOk()) { + return Result::Failure(dof_status); + } + + auto k_ff = ExtractBlock(full_stiffness, dof_manager.FreeDofs(), + dof_manager.FreeDofs()); + if (!k_ff.HasValue()) { + return Result::Failure(k_ff.GetStatus()); + } + auto k_fc = ExtractBlock(full_stiffness, dof_manager.FreeDofs(), + dof_manager.ConstrainedDofs()); + if (!k_fc.HasValue()) { + return Result::Failure(k_fc.GetStatus()); + } + auto k_cf = ExtractBlock(full_stiffness, dof_manager.ConstrainedDofs(), + dof_manager.FreeDofs()); + if (!k_cf.HasValue()) { + return Result::Failure(k_cf.GetStatus()); + } + auto k_cc = ExtractBlock(full_stiffness, dof_manager.ConstrainedDofs(), + dof_manager.ConstrainedDofs()); + if (!k_cc.HasValue()) { + return Result::Failure(k_cc.GetStatus()); + } + + return Result::Success( + {std::move(k_ff.Value()), std::move(k_fc.Value()), + std::move(k_cf.Value()), std::move(k_cc.Value())}); +} + +Vector EssentialConstraintPolicy::GatherFree( + const Vector& full_values, const DofManager& dof_manager) const { + RequireDofOrder(dof_manager); + if (full_values.Size() != dof_manager.FullDofCount()) { + throw std::invalid_argument{ + "Full vector size must match the DofManager full dimension."}; + } + Vector reduced{dof_manager.FreeDofCount()}; + for (std::size_t equation = 0U; equation < dof_manager.FreeDofs().size(); + ++equation) { + reduced[equation] = full_values[dof_manager.FreeDofs()[equation]]; + } + return reduced; +} + +Vector EssentialConstraintPolicy::GatherConstrained( + const Vector& full_values, const DofManager& dof_manager) const { + RequireDofOrder(dof_manager); + if (full_values.Size() != dof_manager.FullDofCount()) { + throw std::invalid_argument{ + "Full vector size must match the DofManager full dimension."}; + } + Vector reduced{dof_manager.ConstrainedDofCount()}; + for (std::size_t index = 0U; index < dof_manager.ConstrainedDofs().size(); + ++index) { + reduced[index] = full_values[dof_manager.ConstrainedDofs()[index]]; + } + return reduced; +} + +Vector EssentialConstraintPolicy::ReconstructFull( + const Vector& free_values, const Vector& constrained_values, + const DofManager& dof_manager) const { + RequireDofOrder(dof_manager); + if (free_values.Size() != dof_manager.FreeDofCount() || + constrained_values.Size() != dof_manager.ConstrainedDofCount()) { + throw std::invalid_argument{ + "Reduced vector sizes must match the DofManager order."}; + } + + Vector full{dof_manager.FullDofCount()}; + for (std::size_t equation = 0U; equation < dof_manager.FreeDofs().size(); + ++equation) { + full[dof_manager.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 < dof_manager.ConstrainedDofs().size(); + ++index) { + full[dof_manager.ConstrainedDofs()[index]] = constrained_values[index]; + } + return full; +} + +} // namespace fesa diff --git a/src/fesa/constraints/essential_constraints.cpp b/src/fesa/constraints/essential_constraints.cpp deleted file mode 100644 index d8a1c51..0000000 --- a/src/fesa/constraints/essential_constraints.cpp +++ /dev/null @@ -1,161 +0,0 @@ -#include "fesa/constraints/essential_constraints.h" - -#include -#include -#include -#include -#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}}); -} - -/// @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.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}); - } - 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 (!dofs.ValidateInvariants().IsOk()) { - throw std::invalid_argument{ - "DofManager constraint dimensions or order are invalid."}; - } -} - -} // namespace - -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 = dofs.ValidateInvariants(); - 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()); - } - - 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::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& 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]] = 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 diff --git a/src/fesa/constraints/prescribed_displacement.cpp b/src/fesa/constraints/prescribed_displacement.cpp new file mode 100644 index 0000000..1dfd18e --- /dev/null +++ b/src/fesa/constraints/prescribed_displacement.cpp @@ -0,0 +1,107 @@ +#include "fesa/constraints/prescribed_displacement.h" + +#include +#include +#include +#include +#include + +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" + +namespace fesa { +namespace { + +Status BoundaryFailure(const std::string& code, const SourceLocation& location, + const std::string& identity, + const std::string& message) { + return Status::Failure( + FailureCategory::kModel, + {{Severity::kError, code, location, "BOUNDARY", identity, message}}); +} + +} // namespace + +PrescribedDisplacementBoundaryCondition:: + PrescribedDisplacementBoundaryCondition(SourceTargetQuery target, + const DofComponent component, + const double prescribed_value, + const std::size_t source_order, + SourceLocation location) + : BoundaryCondition{target.target_name_or_label, std::move(location)}, + target_{std::move(target)}, + component_{component}, + prescribed_value_{prescribed_value}, + source_order_{source_order} {} + +Result> +PrescribedDisplacementBoundaryCondition::ResolveConstraints( + const BoundaryConditionContext& context) const { + const std::string& identity = target_.target_name_or_label; + if (!std::isfinite(prescribed_value_)) { + return Result>::Failure( + BoundaryFailure("nonfinite-prescribed-value", Location(), identity, + "A prescribed displacement value must be finite.")); + } + if (target_.entity_kind != SourceEntityKind::kNode) { + return Result>::Failure( + BoundaryFailure("invalid-boundary-target", Location(), identity, + "A prescribed displacement requires a node target.")); + } + + auto resolved = context.target_resolver.Resolve(target_); + if (!resolved.HasValue()) { + return Result>::Failure(BoundaryFailure( + "invalid-boundary-target", Location(), identity, + "The boundary target must resolve unambiguously to one node or one " + "expanded node set.")); + } + + std::vector seen(context.domain.Nodes().size(), 0U); + std::vector definitions; + definitions.reserve(resolved.Value().size()); + for (const auto& target : resolved.Value()) { + const EntityIndex node = target.entity_index; + if (node >= context.domain.Nodes().size() || seen[node] != 0U) { + return Result>::Failure(BoundaryFailure( + "invalid-boundary-target", Location(), identity, + "The expanded node set must contain unique in-range stable " + "node identities.")); + } + seen[node] = 1U; + try { + definitions.push_back({source_order_, + context.dof_manager.FullDof(node, component_), + prescribed_value_}); + } catch (const std::out_of_range&) { + return Result>::Failure( + BoundaryFailure("invalid-boundary-target", Location(), identity, + "The resolved boundary target must provide the " + "prescribed full DOF.")); + } + } + return Result>::Success( + std::move(definitions)); +} + +const SourceTargetQuery& PrescribedDisplacementBoundaryCondition::Target() + const noexcept { + return target_; +} + +DofComponent PrescribedDisplacementBoundaryCondition::Component() + const noexcept { + return component_; +} + +double PrescribedDisplacementBoundaryCondition::PrescribedValue() + const noexcept { + return prescribed_value_; +} + +std::size_t PrescribedDisplacementBoundaryCondition::SourceOrder() + const noexcept { + return source_order_; +} + +} // namespace fesa diff --git a/src/fesa/fem/dof_manager.cpp b/src/fesa/fem/dof_manager.cpp index ae6c313..55f928a 100644 --- a/src/fesa/fem/dof_manager.cpp +++ b/src/fesa/fem/dof_manager.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -47,21 +48,6 @@ std::vector FullNodeComponents() { DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz}; } -std::vector ExpandBoundaryTarget( - const SourceTargetResolver& resolver, const BoundaryCondition& boundary) { - auto resolved = - resolver.Resolve({SourceEntityKind::kNode, "", boundary.target}); - if (!resolved.HasValue()) { - return {}; - } - std::vector indices; - indices.reserve(resolved.Value().size()); - for (const auto& target : resolved.Value()) { - indices.push_back(target.entity_index); - } - return indices; -} - void AppendScatter(std::vector>& columns_by_row, const std::vector& scatter) { for (const std::size_t row : scatter) { @@ -124,7 +110,8 @@ Result DofManager::Create(const AnalysisModel& model) { } DofManager dofs; - const Status status = dofs.BuildLayouts(model, layouts); + const Status status = + dofs.BuildLayouts(model, layouts, model.Step().BoundaryConditions()); if (!status.IsOk()) { return Result::Failure(status); } @@ -133,16 +120,24 @@ Result DofManager::Create(const AnalysisModel& model) { Status DofManager::Build(const AnalysisModel& analysis_model, const ElementView& elements) { + return Build(analysis_model, elements, + analysis_model.Step().BoundaryConditions()); +} + +Status DofManager::Build(const AnalysisModel& analysis_model, + const ElementView& elements, + const BoundaryConditionView& boundaries) { std::vector layouts; layouts.reserve(elements.size()); for (const auto& element : elements) { layouts.push_back(element.get().DofLayout()); } - return BuildLayouts(analysis_model, layouts); + return BuildLayouts(analysis_model, layouts, boundaries); } Status DofManager::BuildLayouts(const AnalysisModel& analysis_model, - const std::vector& layouts) { + const std::vector& layouts, + const BoundaryConditionView& boundaries) { const Domain& domain = analysis_model.GetDomain(); if (domain.Nodes().size() > (std::numeric_limits::max)() / kDofsPerNode) { @@ -177,29 +172,69 @@ Status DofManager::BuildLayouts(const AnalysisModel& analysis_model, const SourceTargetResolver target_resolver{target_index}; const std::size_t full_count = domain.Nodes().size() * kDofsPerNode; - std::vector> prescribed_by_full_dof(full_count); - for (const EntityIndex boundary_index : - analysis_model.ActiveBoundaryConditions()) { - const auto& boundary = - analysis_model.Step().Boundaries().at(boundary_index); - const auto target = ExpandBoundaryTarget(target_resolver, 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 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; + DofManager full_dof_map; + full_dof_map.full_dof_count_ = full_count; + const BoundaryConditionContext context{domain, full_dof_map, target_resolver}; + std::vector> definitions_by_boundary; + definitions_by_boundary.reserve(boundaries.size()); + for (std::size_t source_order = 0U; source_order < boundaries.size(); + ++source_order) { + auto definitions = + boundaries[source_order].get().ResolveConstraints(context); + if (!definitions.HasValue()) { + return definitions.GetStatus(); + } + std::vector seen_full_dofs; + seen_full_dofs.reserve(definitions.Value().size()); + for (const auto& definition : definitions.Value()) { + if (definition.source_order != source_order) { + return DofFailure( + "invalid-constraint-order", analysis_model.Step().Location(), + std::to_string(definition.source_order), + "Every constraint definition must retain its supplying boundary " + "source order."); } + if (definition.full_dof_index >= full_count) { + return DofFailure( + "invalid-constraint-index", analysis_model.Step().Location(), + std::to_string(definition.full_dof_index), + "A constraint definition full-DOF index is outside the active " + "model."); + } + if (!std::isfinite(definition.prescribed_value)) { + return DofFailure( + "nonfinite-prescribed-value", analysis_model.Step().Location(), + std::to_string(source_order), + "A constraint definition prescribed value must be finite."); + } + if (std::find(seen_full_dofs.begin(), seen_full_dofs.end(), + definition.full_dof_index) != seen_full_dofs.end()) { + return DofFailure( + "duplicate-constraint-definition", analysis_model.Step().Location(), + std::to_string(definition.full_dof_index), + "One boundary must not emit the same full DOF more than once."); + } + seen_full_dofs.push_back(definition.full_dof_index); + } + definitions_by_boundary.push_back(std::move(definitions.Value())); + } + + std::vector> prescribed_by_full_dof(full_count); + for (std::size_t source_order = 0U; + source_order < definitions_by_boundary.size(); ++source_order) { + const auto& definitions = definitions_by_boundary[source_order]; + for (const auto& definition : definitions) { + auto& prescribed = prescribed_by_full_dof[definition.full_dof_index]; + if (prescribed && *prescribed != definition.prescribed_value) { + return Status::Failure( + FailureCategory::kInput, + {{Severity::kError, "conflicting-boundary-condition", + boundaries[source_order].get().Location(), "BOUNDARY", + boundaries[source_order].get().TargetIdentity(), + "Expanded boundary rows prescribe different values to one " + "node/DOF."}}); + } + prescribed = definition.prescribed_value; } } @@ -356,6 +391,12 @@ Status DofManager::ValidateInvariants() const { "Full, free, constrained, prescribed, and equation dimensions must " "agree."); } + for (std::size_t index = 0U; index < prescribed_values_.Size(); ++index) { + if (!std::isfinite(prescribed_values_[index])) { + return DofFailure("nonfinite-prescribed-value", {}, std::to_string(index), + "Prescribed displacement values must remain finite."); + } + } if (HasAdjacentDuplicate(free_dofs_)) { return DofFailure("duplicate-dof-mapping", {}, std::to_string(full_dof_count_), diff --git a/src/fesa/io/abaqus/domain_mapper.cpp b/src/fesa/io/abaqus/domain_mapper.cpp index 087dcb6..07eb4e2 100644 --- a/src/fesa/io/abaqus/domain_mapper.cpp +++ b/src/fesa/io/abaqus/domain_mapper.cpp @@ -108,7 +108,7 @@ struct RawStep { SourceLocation location; bool has_static{false}; std::array static_values{}; - std::vector boundaries; + std::vector boundaries; std::vector loads; }; @@ -1202,8 +1202,9 @@ class MappingContext { material.elastic_location = block.data[0].location; } - void ParseBoundary(const KeywordBlock& block, - std::vector& destination) { + void ParseBoundary( + const KeywordBlock& block, + std::vector& destination) { if (!ValidateParameters(block, {}) || block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, @@ -2086,7 +2087,8 @@ class MappingContext { } void FinalizeStep() { - std::vector boundaries = model_boundaries_; + std::vector boundaries = + model_boundaries_; boundaries.insert(boundaries.end(), step_.boundaries.begin(), step_.boundaries.end()); const SourceTargetIndex target_index = BuildSourceTargetIndex(); @@ -2138,7 +2140,7 @@ class MappingContext { std::vector instances_; std::vector assembly_sets_; std::vector materials_; - std::vector model_boundaries_; + std::vector model_boundaries_; RawStep step_{}; std::vector>> part_section_assignments_; diff --git a/src/fesa/model/domain.cpp b/src/fesa/model/domain.cpp index bdcaa1f..60f2c5a 100644 --- a/src/fesa/model/domain.cpp +++ b/src/fesa/model/domain.cpp @@ -9,12 +9,24 @@ namespace fesa { StepDefinition::StepDefinition(StaticStepDefinition definition) : name_{std::move(definition.name)}, - boundaries_{std::move(definition.boundaries)}, initial_increment_{definition.initial_increment}, time_period_{definition.time_period}, minimum_increment_{definition.minimum_increment}, maximum_increment_{definition.maximum_increment}, location_{std::move(definition.location)} { + for (auto& boundary : definition.boundaries) { + for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof; + ++source_dof) { + const std::size_t source_order = boundary_conditions_.size(); + auto owned = std::make_unique( + SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target}, + static_cast(source_dof - 1), boundary.value, + source_order, boundary.location); + boundary_conditions_view_.push_back(std::cref(*owned)); + prescribed_displacements_view_.Add(*owned); + boundary_conditions_.push_back(std::move(owned)); + } + } loads_.reserve(definition.loads.size()); loads_view_.reserve(definition.loads.size()); for (std::size_t source_order = 0U; source_order < definition.loads.size(); @@ -31,9 +43,14 @@ StepDefinition::StepDefinition(StaticStepDefinition definition) const std::string& StepDefinition::Name() const noexcept { return name_; } -const std::vector& StepDefinition::Boundaries() +const BoundaryConditionView& StepDefinition::BoundaryConditions() const noexcept { - return boundaries_; + return boundary_conditions_view_; +} + +const DomainCollectionView& +StepDefinition::PrescribedDisplacements() const noexcept { + return prescribed_displacements_view_; } const LoadView& StepDefinition::Loads() const noexcept { return loads_view_; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 1e3181a..4ac90cd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ add_executable( unit/assembly/parallel_for_test.cpp unit/assembly/load_assembler_test.cpp unit/assembly/sparse_assembler_test.cpp + unit/constraints/boundary_condition_test.cpp unit/constraints/essential_constraints_test.cpp unit/core/ascii_test.cpp unit/core/diagnostic_test.cpp diff --git a/tests/unit/analysis/analysis_model_test.cpp b/tests/unit/analysis/analysis_model_test.cpp index d8abbd6..81cc32a 100644 --- a/tests/unit/analysis/analysis_model_test.cpp +++ b/tests/unit/analysis/analysis_model_test.cpp @@ -107,7 +107,7 @@ TEST(AnalysisModel, ClassifiesActiveEntitiesInStableOrder) { EXPECT_EQ(model.ActiveSections(), (std::vector{0U, 1U, 2U})); EXPECT_EQ(model.ActiveBoundaryConditions(), - (std::vector{0U, 1U})); + (std::vector{0U, 1U, 2U, 3U, 4U, 5U})); EXPECT_EQ(model.ActiveLoads(), (std::vector{0U, 1U, 2U})); ASSERT_EQ(model.Step().Loads().size(), 3U); EXPECT_EQ(&model.Step().Loads()[0U].get(), diff --git a/tests/unit/assembly/load_assembler_test.cpp b/tests/unit/assembly/load_assembler_test.cpp index ae7938f..b8f5ad8 100644 --- a/tests/unit/assembly/load_assembler_test.cpp +++ b/tests/unit/assembly/load_assembler_test.cpp @@ -27,10 +27,10 @@ struct LoadFixture { std::unique_ptr dofs; }; -LoadFixture MakeFixture(const std::size_t node_count, - std::vector node_sets, - std::vector boundaries, - std::vector loads) { +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; @@ -73,8 +73,9 @@ LoadFixture MakeFixture(const std::size_t node_count, return {std::move(domain), std::move(model), std::move(dofs)}; } -LoadFixture MakeShellFixture(std::vector boundaries, - std::vector loads) { +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; diff --git a/tests/unit/constraints/boundary_condition_test.cpp b/tests/unit/constraints/boundary_condition_test.cpp new file mode 100644 index 0000000..6e98e2e --- /dev/null +++ b/tests/unit/constraints/boundary_condition_test.cpp @@ -0,0 +1,217 @@ +#include "fesa/constraints/boundary_condition.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "fesa/analysis/analysis_model.h" +#include "fesa/constraints/prescribed_displacement.h" +#include "fesa/fem/dof_manager.h" +#include "fesa/model/domain.h" +#include "fesa/model/source_target_resolver.h" + +namespace { + +struct BoundaryFixture { + std::unique_ptr domain; + std::unique_ptr model; + std::unique_ptr dofs; +}; + +fesa::ModelDefinition MakeDefinition( + std::vector boundaries = {}) { + const std::filesystem::path source{"models/boundary-condition.inp"}; + fesa::ModelDefinition definition{}; + definition.source_path = source; + definition.source_content_identity = "fnv1a64:boundary"; + definition.nodes = {{{"Beam-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 2U}}, + {{"Beam-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 3U}}}; + definition.node_sets = { + {"Pair", std::string{"Beam-1"}, {1U, 0U}, {source, 4U}}}; + definition.steps = {{"Step-1", + std::move(boundaries), + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 10U}}}; + return definition; +} + +BoundaryFixture MakeFixture( + std::vector boundaries = {}) { + auto domain_result = + fesa::Domain::Create(MakeDefinition(std::move(boundaries))); + EXPECT_TRUE(domain_result.HasValue()); + auto domain = + std::make_unique(std::move(domain_result.Value())); + + auto model_result = fesa::AnalysisModel::Create(*domain); + EXPECT_TRUE(model_result.HasValue()); + auto model = + std::make_unique(std::move(model_result.Value())); + + auto dof_result = fesa::DofManager::Create(*model); + EXPECT_TRUE(dof_result.HasValue()); + auto dofs = std::make_unique(std::move(dof_result.Value())); + return {std::move(domain), std::move(model), std::move(dofs)}; +} + +class FakeBoundaryCondition final : public fesa::BoundaryCondition { + public: + explicit FakeBoundaryCondition( + std::vector definitions) + : definitions_{std::move(definitions)} {} + + fesa::Result> ResolveConstraints( + const fesa::BoundaryConditionContext&) const override { + return fesa::Result>::Success( + definitions_); + } + + private: + std::vector definitions_; +}; + +void ExpectFailureCode(const fesa::Status& status, const std::string& code) { + ASSERT_FALSE(status.IsOk()); + ASSERT_EQ(status.Diagnostics().size(), 1U); + EXPECT_EQ(status.Diagnostics()[0U].code, code); +} + +void ExpectCandidate(const fesa::DofManager& dofs, + const std::vector& constrained_dofs, + const std::vector& prescribed_values) { + EXPECT_EQ(dofs.ConstrainedDofs(), constrained_dofs); + ASSERT_EQ(dofs.PrescribedValues().Size(), prescribed_values.size()); + for (std::size_t index = 0U; index < prescribed_values.size(); ++index) { + EXPECT_DOUBLE_EQ(dofs.PrescribedValues()[index], prescribed_values[index]); + } +} + +} // namespace + +// C-BC-001 +TEST(BoundaryCondition, ResolvesStableNonzeroDefinitionsThroughBase) { + static_assert(std::has_virtual_destructor_v); + auto fixture = MakeFixture(); + const fesa::SourceTargetIndex target_index = + fesa::SourceTargetIndex::FromDomain(*fixture.domain); + const fesa::SourceTargetResolver resolver{target_index}; + const fesa::BoundaryConditionContext context{*fixture.domain, *fixture.dofs, + resolver}; + const fesa::PrescribedDisplacementBoundaryCondition prescribed( + {fesa::SourceEntityKind::kNode, "Beam-1", "pair"}, + fesa::DofComponent::kUz, 2.5, 4U); + const fesa::BoundaryCondition& boundary = prescribed; + + const auto definitions = boundary.ResolveConstraints(context); + + ASSERT_TRUE(definitions.HasValue()); + ASSERT_EQ(definitions.Value().size(), 2U); + EXPECT_EQ(definitions.Value()[0U].source_order, 4U); + EXPECT_EQ(definitions.Value()[0U].full_dof_index, 8U); + EXPECT_DOUBLE_EQ(definitions.Value()[0U].prescribed_value, 2.5); + EXPECT_EQ(definitions.Value()[1U].source_order, 4U); + EXPECT_EQ(definitions.Value()[1U].full_dof_index, 2U); + EXPECT_DOUBLE_EQ(definitions.Value()[1U].prescribed_value, 2.5); +} + +TEST(BoundaryCondition, DomainOwnsExpandedPolymorphicDefinitionsStably) { + const std::filesystem::path source{"models/boundary-condition.inp"}; + auto fixture = MakeFixture({{"Pair", 1, 2, 1.25, {source, 12U}}}); + + const auto& boundaries = fixture.model->Step().BoundaryConditions(); + ASSERT_EQ(boundaries.size(), 2U); + EXPECT_EQ(fixture.model->ActiveBoundaryConditions(), + (std::vector{0U, 1U})); + + const fesa::SourceTargetIndex target_index = + fesa::SourceTargetIndex::FromDomain(*fixture.domain); + const fesa::SourceTargetResolver resolver{target_index}; + const fesa::BoundaryConditionContext context{*fixture.domain, *fixture.dofs, + resolver}; + const auto first = boundaries[0U].get().ResolveConstraints(context); + const auto second = boundaries[1U].get().ResolveConstraints(context); + ASSERT_TRUE(first.HasValue()); + ASSERT_TRUE(second.HasValue()); + EXPECT_EQ(first.Value()[0U].source_order, 0U); + EXPECT_EQ(first.Value()[0U].full_dof_index, 6U); + EXPECT_EQ(second.Value()[0U].source_order, 1U); + EXPECT_EQ(second.Value()[0U].full_dof_index, 7U); + EXPECT_DOUBLE_EQ(first.Value()[0U].prescribed_value, 1.25); + EXPECT_DOUBLE_EQ(second.Value()[0U].prescribed_value, 1.25); +} + +TEST(BoundaryCondition, + RejectsDuplicateConflictAndNonfiniteBeforeCandidateCommit) { + auto fixture = MakeFixture(); + const fesa::ElementView elements; + FakeBoundaryCondition initial({{0U, 1U, 3.0}}); + ASSERT_TRUE( + fixture.dofs->Build(*fixture.model, elements, {std::cref(initial)}) + .IsOk()); + ExpectCandidate(*fixture.dofs, {1U}, {3.0}); + + FakeBoundaryCondition duplicate({{0U, 2U, 4.0}, {0U, 2U, 4.0}}); + ExpectFailureCode( + fixture.dofs->Build(*fixture.model, elements, {std::cref(duplicate)}), + "duplicate-constraint-definition"); + ExpectCandidate(*fixture.dofs, {1U}, {3.0}); + + FakeBoundaryCondition first({{0U, 2U, 4.0}}); + FakeBoundaryCondition conflicting({{1U, 2U, -4.0}}); + ExpectFailureCode( + fixture.dofs->Build(*fixture.model, elements, + {std::cref(first), std::cref(conflicting)}), + "conflicting-boundary-condition"); + ExpectCandidate(*fixture.dofs, {1U}, {3.0}); + + FakeBoundaryCondition nonfinite( + {{0U, 2U, std::numeric_limits::quiet_NaN()}}); + ExpectFailureCode( + fixture.dofs->Build(*fixture.model, elements, {std::cref(nonfinite)}), + "nonfinite-prescribed-value"); + ExpectCandidate(*fixture.dofs, {1U}, {3.0}); +} + +TEST(BoundaryCondition, AcceptsIdenticalOverlapAcrossSourceDefinitions) { + auto fixture = MakeFixture(); + FakeBoundaryCondition first({{0U, 4U, -2.0}}); + FakeBoundaryCondition second({{1U, 4U, -2.0}}); + + const fesa::Status status = fixture.dofs->Build( + *fixture.model, {}, {std::cref(first), std::cref(second)}); + + ASSERT_TRUE(status.IsOk()); + ExpectCandidate(*fixture.dofs, {4U}, {-2.0}); +} + +TEST(BoundaryCondition, ConcreteRejectsNonfinitePrescribedValue) { + auto fixture = MakeFixture(); + const fesa::SourceTargetIndex target_index = + fesa::SourceTargetIndex::FromDomain(*fixture.domain); + const fesa::SourceTargetResolver resolver{target_index}; + const fesa::BoundaryConditionContext context{*fixture.domain, *fixture.dofs, + resolver}; + const fesa::PrescribedDisplacementBoundaryCondition prescribed( + {fesa::SourceEntityKind::kNode, "Beam-1", "10"}, fesa::DofComponent::kUx, + std::numeric_limits::infinity(), 0U); + + const auto result = prescribed.ResolveConstraints(context); + + ASSERT_FALSE(result.HasValue()); + ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); + EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code, + "nonfinite-prescribed-value"); +} diff --git a/tests/unit/constraints/essential_constraints_test.cpp b/tests/unit/constraints/essential_constraints_test.cpp index 5e03c6c..166f0d0 100644 --- a/tests/unit/constraints/essential_constraints_test.cpp +++ b/tests/unit/constraints/essential_constraints_test.cpp @@ -1,5 +1,3 @@ -#include "fesa/constraints/essential_constraints.h" - #include #include @@ -10,12 +8,14 @@ #include #include "fesa/analysis/analysis_model.h" +#include "fesa/constraints/essential_constraint_policy.h" #include "fesa/fem/dof_manager.h" #include "fesa/model/domain.h" namespace { -fesa::DofManager MakeDofs(std::vector boundaries) { +fesa::DofManager MakeDofs( + std::vector boundaries) { const std::filesystem::path source{"models/essential-constraints.inp"}; fesa::ModelDefinition definition{}; definition.source_path = source; @@ -40,7 +40,7 @@ fesa::DofManager MakeDofs(std::vector boundaries) { } fesa::DofManager MakeShellSizedDofs( - std::vector boundaries) { + std::vector boundaries) { const std::filesystem::path source{"models/shell-essential-constraints.inp"}; fesa::ModelDefinition definition{}; definition.source_path = source; @@ -124,93 +124,98 @@ TEST(EssentialConstraints, ExtractsHandComputedBlocksInStableOrder) { full_values[2U * 6U + 4U] = 0.0; const auto full = MakeMatrix(6U, 6U, full_values); - auto result = fesa::EssentialConstraints::Partition(full, dofs); + const fesa::EssentialConstraintPolicy policy; + auto result = policy.Partition(full, dofs); ASSERT_TRUE(result.HasValue()); const auto& blocks = result.Value(); - EXPECT_EQ(blocks.kff.RowOffsets(), + EXPECT_EQ(blocks.k_ff.RowOffsets(), (std::vector{0U, 4U, 8U, 12U, 16U})); - EXPECT_EQ(blocks.kff.ColumnIndices(), + EXPECT_EQ(blocks.k_ff.ColumnIndices(), (std::vector{0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U, 0U, 1U, 2U, 3U})); EXPECT_EQ( - blocks.kff.Values(), + blocks.k_ff.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(), + EXPECT_EQ(blocks.k_fc.RowOffsets(), (std::vector{0U, 2U, 4U, 6U, 8U})); - EXPECT_EQ(blocks.kfc.ColumnIndices(), + EXPECT_EQ(blocks.k_fc.ColumnIndices(), (std::vector{0U, 1U, 0U, 1U, 0U, 1U, 0U, 1U})); - EXPECT_EQ(blocks.kfc.Values(), + EXPECT_EQ(blocks.k_fc.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(), + EXPECT_EQ(blocks.k_cf.RowOffsets(), (std::vector{0U, 4U, 8U})); + EXPECT_EQ(blocks.k_cf.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(), + EXPECT_EQ( + blocks.k_cf.Values(), + (std::vector{11.0, 13.0, 14.0, 16.0, 41.0, 43.0, 44.0, 46.0})); + EXPECT_EQ(blocks.k_cc.RowOffsets(), (std::vector{0U, 2U, 4U})); + EXPECT_EQ(blocks.k_cc.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.k_cc.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.k_fc.Values()[3U], 0.0); + EXPECT_TRUE(blocks.k_ff.Validate().IsOk()); + EXPECT_TRUE(blocks.k_fc.Validate().IsOk()); + EXPECT_TRUE(blocks.k_cf.Validate().IsOk()); + EXPECT_TRUE(blocks.k_cc.Validate().IsOk()); } TEST(EssentialConstraints, HandlesNoAllAndMixedConstraints) { const auto full = MakeMatrix(6U, 6U, SequentialDense(6U)); + const fesa::EssentialConstraintPolicy policy; const auto no_constraints = MakeDofs({}); - auto none = fesa::EssentialConstraints::Partition(full, no_constraints); + auto none = policy.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()); + ExpectShape(none.Value().k_ff, 6U, 6U); + ExpectShape(none.Value().k_fc, 6U, 0U); + ExpectShape(none.Value().k_cf, 0U, 6U); + ExpectShape(none.Value().k_cc, 0U, 0U); + EXPECT_EQ(none.Value().k_ff.Values(), full.Values()); const auto all_constraints = MakeDofs({{"1", 1, 6, 1.0, {{}, 12U}}}); - auto all = fesa::EssentialConstraints::Partition(full, all_constraints); + auto all = policy.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()); + ExpectShape(all.Value().k_ff, 0U, 0U); + ExpectShape(all.Value().k_fc, 0U, 6U); + ExpectShape(all.Value().k_cf, 6U, 0U); + ExpectShape(all.Value().k_cc, 6U, 6U); + EXPECT_EQ(all.Value().k_cc.Values(), full.Values()); const auto mixed_constraints = MakeDofs({{"1", 3, 4, 0.0, {{}, 12U}}}); - auto mixed = fesa::EssentialConstraints::Partition(full, mixed_constraints); + auto mixed = policy.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); + ExpectShape(mixed.Value().k_ff, 4U, 4U); + ExpectShape(mixed.Value().k_fc, 4U, 2U); + ExpectShape(mixed.Value().k_cf, 2U, 4U); + ExpectShape(mixed.Value().k_cc, 2U, 2U); } // MITC4-DOF-003 TEST(EssentialConstraints, PreservesShellSizedNoMixedAndAllConstraintRoundTrips) { const auto full = MakeMatrix(24U, 24U, SequentialDense(24U)); + const fesa::EssentialConstraintPolicy policy; const auto no_constraints = MakeShellSizedDofs({}); - auto none = fesa::EssentialConstraints::Partition(full, no_constraints); + auto none = policy.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); + ExpectShape(none.Value().k_ff, 24U, 24U); + ExpectShape(none.Value().k_fc, 24U, 0U); + ExpectShape(none.Value().k_cf, 0U, 24U); + ExpectShape(none.Value().k_cc, 0U, 0U); 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); + auto mixed = policy.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); + ExpectShape(mixed.Value().k_ff, 17U, 17U); + ExpectShape(mixed.Value().k_fc, 17U, 7U); + ExpectShape(mixed.Value().k_cf, 7U, 17U); + ExpectShape(mixed.Value().k_cc, 7U, 7U); fesa::Vector mixed_full{24U}; for (std::size_t index = 0U; index < mixed_full.Size(); ++index) { @@ -221,9 +226,8 @@ TEST(EssentialConstraints, 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( + const auto mixed_free = policy.GatherFree(mixed_full, mixed_constraints); + const auto mixed_reconstructed = policy.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) { @@ -235,14 +239,14 @@ TEST(EssentialConstraints, {"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); + auto all = policy.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); + ExpectShape(all.Value().k_ff, 0U, 0U); + ExpectShape(all.Value().k_fc, 0U, 24U); + ExpectShape(all.Value().k_cf, 24U, 0U); + ExpectShape(all.Value().k_cc, 24U, 24U); - const auto all_reconstructed = fesa::EssentialConstraints::ReconstructFull( + const auto all_reconstructed = policy.ReconstructFull( fesa::Vector{0U}, all_constraints.PrescribedValues(), all_constraints); ASSERT_EQ(all_reconstructed.Size(), 24U); for (std::size_t node = 0U; node < 4U; ++node) { @@ -253,6 +257,7 @@ TEST(EssentialConstraints, } TEST(EssentialConstraints, ReconstructsNonzeroPrescribedValues) { + const fesa::EssentialConstraintPolicy policy; const auto dofs = MakeDofs({{"1", 2, 2, 2.5, {{}, 12U}}, {"1", 5, 5, -3.25, {{}, 13U}}}); fesa::Vector full{6U}; @@ -263,9 +268,8 @@ TEST(EssentialConstraints, ReconstructsNonzeroPrescribedValues) { full[4U] = -3.25; full[5U] = 40.0; - const auto free = fesa::EssentialConstraints::GatherFree(full, dofs); - const auto constrained = - fesa::EssentialConstraints::GatherConstrained(full, dofs); + const auto free = policy.GatherFree(full, dofs); + const auto constrained = policy.GatherConstrained(full, dofs); EXPECT_EQ(free.Size(), 4U); EXPECT_DOUBLE_EQ(free[0U], 10.0); EXPECT_DOUBLE_EQ(free[1U], 20.0); @@ -277,8 +281,8 @@ TEST(EssentialConstraints, ReconstructsNonzeroPrescribedValues) { EXPECT_EQ(constrained[0U], dofs.PrescribedValues()[0U]); EXPECT_EQ(constrained[1U], dofs.PrescribedValues()[1U]); - const auto reconstructed = fesa::EssentialConstraints::ReconstructFull( - free, dofs.PrescribedValues(), dofs); + const auto reconstructed = + policy.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]); @@ -286,10 +290,10 @@ TEST(EssentialConstraints, ReconstructsNonzeroPrescribedValues) { } TEST(EssentialConstraints, RejectsDimensionOrOrderMismatch) { + const fesa::EssentialConstraintPolicy policy; 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); + auto wrong_dimension = policy.Partition(wrong_square, dofs); ASSERT_FALSE(wrong_dimension.HasValue()); EXPECT_EQ(wrong_dimension.GetStatus().Category(), fesa::FailureCategory::kModel); @@ -298,18 +302,17 @@ TEST(EssentialConstraints, RejectsDimensionOrOrderMismatch) { "invalid-constraint-dimensions"); const auto rectangular = MakeMatrix(6U, 5U, std::vector(30U, 0.0)); - auto wrong_order = fesa::EssentialConstraints::Partition(rectangular, dofs); + auto wrong_order = policy.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)), + EXPECT_THROW(static_cast(policy.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( + EXPECT_THROW( + static_cast(policy.GatherConstrained(fesa::Vector{7U}, dofs)), + std::invalid_argument); + EXPECT_THROW(static_cast(policy.ReconstructFull( fesa::Vector{4U}, fesa::Vector{2U}, dofs)), std::invalid_argument); } diff --git a/tests/unit/io/abaqus/domain_mapper_test.cpp b/tests/unit/io/abaqus/domain_mapper_test.cpp index 0ec6e7c..a022b41 100644 --- a/tests/unit/io/abaqus/domain_mapper_test.cpp +++ b/tests/unit/io/abaqus/domain_mapper_test.cpp @@ -13,6 +13,7 @@ #include #include +#include "fesa/elements/element.h" #include "fesa/elements/element_definition.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/math/vector3.h" @@ -292,12 +293,15 @@ TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) { EXPECT_DOUBLE_EQ(step.TimePeriod(), 1.5); EXPECT_DOUBLE_EQ(step.MinimumIncrement(), 0.01); EXPECT_DOUBLE_EQ(step.MaximumIncrement(), 1.5); - ASSERT_EQ(step.Boundaries().size(), 2U); - EXPECT_EQ(step.Boundaries()[0].target, "rootassembly"); - EXPECT_EQ(step.Boundaries()[0].first_dof, 1); - EXPECT_DOUBLE_EQ(step.Boundaries()[0].value, 0.125); - EXPECT_EQ(step.Boundaries()[1].last_dof, 2); - EXPECT_DOUBLE_EQ(step.Boundaries()[1].value, 0.0); + ASSERT_EQ(step.PrescribedDisplacements().Size(), 2U); + EXPECT_EQ(step.PrescribedDisplacements()[0U].Target().target_name_or_label, + "rootassembly"); + EXPECT_EQ(step.PrescribedDisplacements()[0U].Component(), + fesa::DofComponent::kUx); + EXPECT_DOUBLE_EQ(step.PrescribedDisplacements()[0U].PrescribedValue(), 0.125); + EXPECT_EQ(step.PrescribedDisplacements()[1U].Component(), + fesa::DofComponent::kUy); + EXPECT_DOUBLE_EQ(step.PrescribedDisplacements()[1U].PrescribedValue(), 0.0); ASSERT_EQ(step.ConcentratedLoads().Size(), 1U); EXPECT_EQ(step.ConcentratedLoads()[0U].Target().target_name_or_label, "RootAssembly"); @@ -411,7 +415,11 @@ OnlySecond, 2, 5. (std::vector{1U})); ASSERT_EQ(domain.Steps().Size(), 1U); - EXPECT_EQ(domain.Steps()[0].Boundaries()[0].target, "OnlySecond"); + EXPECT_EQ(domain.Steps()[0] + .PrescribedDisplacements()[0U] + .Target() + .target_name_or_label, + "OnlySecond"); EXPECT_EQ( domain.Steps()[0].ConcentratedLoads()[0U].Target().target_name_or_label, "OnlySecond"); @@ -421,7 +429,12 @@ OnlySecond, 2, 5. ReplaceOnce(ReplaceOnce(MinimalDeck(), "Root, 1, 6", "1, 1, 6"), "Tip, 2, -1.", "2, 2, -1.")); ASSERT_TRUE(direct.HasValue()); - EXPECT_EQ(direct.Value().Steps()[0].Boundaries()[0].target, "1"); + EXPECT_EQ(direct.Value() + .Steps()[0] + .PrescribedDisplacements()[0U] + .Target() + .target_name_or_label, + "1"); EXPECT_EQ(direct.Value() .Steps()[0] .ConcentratedLoads()[0U] @@ -551,8 +564,8 @@ TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) { EXPECT_EQ(with_no_ops.Value().ElementSets().size(), plain.Value().ElementSets().size()); EXPECT_EQ(with_no_ops.Value().Steps().Size(), plain.Value().Steps().Size()); - EXPECT_EQ(with_no_ops.Value().Steps()[0].Boundaries().size(), - plain.Value().Steps()[0].Boundaries().size()); + EXPECT_EQ(with_no_ops.Value().Steps()[0].BoundaryConditions().size(), + plain.Value().Steps()[0].BoundaryConditions().size()); EXPECT_EQ(with_no_ops.Value().Steps()[0].Loads().size(), plain.Value().Steps()[0].Loads().size()); } @@ -613,8 +626,9 @@ TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) { (std::array{0.0, 1.0, 0.0})); ASSERT_EQ(domain.Steps().Size(), 1U); - ASSERT_EQ(domain.Steps()[0].Boundaries().size(), 1U); - EXPECT_EQ(domain.Steps()[0].Boundaries()[0].last_dof, 6); + ASSERT_EQ(domain.Steps()[0].PrescribedDisplacements().Size(), 6U); + EXPECT_EQ(domain.Steps()[0].PrescribedDisplacements()[5U].Component(), + fesa::DofComponent::kUrz); ASSERT_EQ(domain.Steps()[0].ConcentratedLoads().Size(), 1U); EXPECT_DOUBLE_EQ( domain.Steps()[0].ConcentratedLoads()[0U].GlobalComponents()[5U], 1.0); diff --git a/tests/unit/model/domain_test.cpp b/tests/unit/model/domain_test.cpp index b441316..f4c79e5 100644 --- a/tests/unit/model/domain_test.cpp +++ b/tests/unit/model/domain_test.cpp @@ -197,7 +197,7 @@ TEST(DomainModel, ImmutableOwnershipPreservesStableOrder) { EXPECT_DOUBLE_EQ(domain.Steps()[0].TimePeriod(), 1.0); EXPECT_DOUBLE_EQ(domain.Steps()[0].MinimumIncrement(), 1.0e-5); EXPECT_DOUBLE_EQ(domain.Steps()[0].MaximumIncrement(), 1.0); - ASSERT_EQ(domain.Steps()[0].Boundaries().size(), 1U); + ASSERT_EQ(domain.Steps()[0].BoundaryConditions().size(), 6U); ASSERT_EQ(domain.Steps()[0].Loads().size(), 1U); ASSERT_EQ(domain.Steps()[0].ConcentratedLoads().Size(), 1U); EXPECT_DOUBLE_EQ( diff --git a/tests/unit/model/model_types_test.cpp b/tests/unit/model/model_types_test.cpp index 81c2430..994cb4c 100644 --- a/tests/unit/model/model_types_test.cpp +++ b/tests/unit/model/model_types_test.cpp @@ -66,7 +66,7 @@ TEST(DomainModel, SourceAndInternalIdentityRemainDistinct) { fesa::EntityIndex{2}, fesa::EntityIndex{4}, {"models/beam.inp", 20U}}; - const fesa::BoundaryCondition boundary{ + const fesa::PrescribedDisplacementDefinition boundary{ "Root", 1, 6, 0.0, {"models/beam.inp", 60U}}; const fesa::NodalLoad load{"Tip", 3, -1000.0, {"models/beam.inp", 70U}}; const fesa::StaticStepDefinition step{ diff --git a/tests/unit/results/result_recovery_test.cpp b/tests/unit/results/result_recovery_test.cpp index baee64d..67ea101 100644 --- a/tests/unit/results/result_recovery_test.cpp +++ b/tests/unit/results/result_recovery_test.cpp @@ -332,12 +332,14 @@ fesa::ModelDefinition MakeShellDefinition( } definition.steps = { {"Step-1", - constrain_all ? std::vector{{"All", - 1, - 6, - 0.0, - {source, 60U}}} - : std::vector{}, + constrain_all + ? std::vector{{"All", + 1, + 6, + 0.0, + {source, + 60U}}} + : std::vector{}, std::move(loads), 0.1, 1.0,