#include "fesa/fem/dof_manager.h" #include #include #include #include #include #include #include #include #include "fesa/model/source_target_resolver.h" namespace fesa { namespace { constexpr std::size_t kDofsPerNode = 6U; Status DofFailure(const std::string& code, const SourceLocation& location, const std::string& identity, const std::string& message) { return Status::Failure( FailureCategory::kModel, {{Severity::kError, code, location, "DOF_MANAGER", identity, message}}); } 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; } 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 HasAdjacentDuplicate(const std::vector& values) { return std::adjacent_find(values.begin(), values.end()) != values.end(); } std::vector FullNodeComponents() { return {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz, 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) { 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( const std::size_t full_dof_count, const std::vector>& element_scatters) { std::vector> columns_by_row(full_dof_count); for (const auto& scatter : 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; } template std::array FixedScatter( const std::vector& scatter) { if (scatter.size() != kSize) { throw std::out_of_range{ "Stored element scatter does not match the compatibility shape."}; } std::array fixed{}; std::copy(scatter.begin(), scatter.end(), fixed.begin()); return fixed; } } // namespace Result DofManager::Create(const AnalysisModel& model) { const Domain& domain = model.GetDomain(); std::vector layouts; layouts.reserve(model.ActiveElements().size()); for (const EntityIndex element_index : model.ActiveElements()) { if (element_index >= domain.Elements().Size()) { return Result::Failure( DofFailure("invalid-element-layout-order", {domain.SourcePath(), 0U}, std::to_string(element_index), "An active element index is outside the Domain.")); } const auto& definition = domain.Elements()[element_index]; layouts.push_back({definition.SourceId(), definition.NodeIndices(), FullNodeComponents()}); } DofManager dofs; const Status status = dofs.BuildLayouts(model, layouts); if (!status.IsOk()) { return Result::Failure(status); } return Result::Success(std::move(dofs)); } Status DofManager::Build(const AnalysisModel& analysis_model, const ElementView& elements) { std::vector layouts; layouts.reserve(elements.size()); for (const auto& element : elements) { layouts.push_back(element.get().DofLayout()); } return BuildLayouts(analysis_model, layouts); } Status DofManager::BuildLayouts(const AnalysisModel& analysis_model, const std::vector& layouts) { const Domain& domain = analysis_model.GetDomain(); if (domain.Nodes().size() > (std::numeric_limits::max)() / kDofsPerNode) { return DofFailure( "invalid-dof-dimensions", {domain.SourcePath(), 0U}, std::to_string(domain.Nodes().size()), "The node count cannot be represented in full-DOF storage."); } if (layouts.size() != analysis_model.ActiveElements().size()) { return DofFailure( "invalid-element-layout-order", {domain.SourcePath(), 0U}, std::to_string(layouts.size()), "Runtime elements must match the active element inventory."); } for (std::size_t source_order = 0U; source_order < layouts.size(); ++source_order) { const EntityIndex definition_index = analysis_model.ActiveElements()[source_order]; if (definition_index >= domain.Elements().Size() || !SameSourceIdentity(layouts[source_order].source_id, domain.Elements()[definition_index].SourceId())) { return DofFailure( "invalid-element-layout-order", {domain.SourcePath(), 0U}, std::to_string(source_order), "Runtime element layouts must preserve active source order and " "identity."); } } const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain); 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; } } } 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); } } Vector prescribed_values{constrained_values.size()}; for (std::size_t index = 0U; index < constrained_values.size(); ++index) { prescribed_values[index] = constrained_values[index]; } DofManager candidate{full_count, std::move(free_equations), {}, std::move(free_dofs), std::move(constrained_dofs), std::move(prescribed_values), {}}; candidate.element_scatters_.reserve(layouts.size()); for (const auto& layout : layouts) { auto scatter = candidate.ElementScatter(layout); if (!scatter.HasValue()) { return scatter.GetStatus(); } candidate.element_scatters_.push_back(std::move(scatter.Value())); } candidate.sparse_pattern_ = BuildSparsePattern(full_count, candidate.element_scatters_); const Status invariant_status = candidate.ValidateInvariants(); if (!invariant_status.IsOk()) { return invariant_status; } *this = std::move(candidate); return Status::Ok(); } std::size_t DofManager::FullDofCount() const noexcept { return full_dof_count_; } std::size_t DofManager::FreeDofCount() const noexcept { return free_dofs_.size(); } std::size_t DofManager::ConstrainedDofCount() const noexcept { return constrained_dofs_.size(); } std::size_t DofManager::FullDof(const EntityIndex node, const 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::optional DofManager::FreeEquation( const std::size_t full_dof) const { return free_equations_.at(full_dof); } Result> DofManager::ElementScatter( const ElementDofLayout& layout) const { if (layout.node_indices.empty() || layout.components_per_node.empty() || layout.node_indices.size() > (std::numeric_limits::max)() / layout.components_per_node.size()) { return Result>::Failure(DofFailure( "invalid-element-dof-layout", {}, layout.source_id.source_label_text, "An element DOF layout requires a representable nonempty topology " "and component inventory.")); } std::vector scatter; scatter.reserve(layout.node_indices.size() * layout.components_per_node.size()); for (const EntityIndex node : layout.node_indices) { for (const DofComponent component : layout.components_per_node) { const std::size_t component_index = static_cast(component); if (node >= full_dof_count_ / kDofsPerNode || component_index >= kDofsPerNode) { return Result>::Failure(DofFailure( "invalid-element-dof-layout", {}, layout.source_id.source_label_text, "Element node and component identities must resolve in the full " "DOF range.")); } const std::size_t full_dof = static_cast(node) * kDofsPerNode + component_index; if (std::find(scatter.begin(), scatter.end(), full_dof) != scatter.end()) { return Result>::Failure(DofFailure( "duplicate-element-dof", {}, layout.source_id.source_label_text, "An element layout must not repeat a full DOF.")); } scatter.push_back(full_dof); } } return Result>::Success(std::move(scatter)); } std::array DofManager::ElementScatter( const EntityIndex element) const { return FixedScatter<12U>(element_scatters_.at(element)); } std::array DofManager::ShellElementScatter( const EntityIndex element) const { return FixedScatter<24U>(element_scatters_.at(element)); } const std::vector& DofManager::FreeDofs() const noexcept { return free_dofs_; } const std::vector& DofManager::ConstrainedDofs() const noexcept { return constrained_dofs_; } const Vector& DofManager::PrescribedValues() const noexcept { return prescribed_values_; } const SparsePattern& DofManager::GetSparsePattern() const noexcept { return sparse_pattern_; } Status DofManager::ValidateInvariants() const { if (free_equations_.size() != full_dof_count_ || free_dofs_.size() > full_dof_count_ || constrained_dofs_.size() > full_dof_count_ || free_dofs_.size() + constrained_dofs_.size() != full_dof_count_ || prescribed_values_.Size() != constrained_dofs_.size()) { return DofFailure( "invalid-dof-dimensions", {}, std::to_string(full_dof_count_), "Full, free, constrained, prescribed, and equation dimensions must " "agree."); } if (HasAdjacentDuplicate(free_dofs_)) { return DofFailure("duplicate-dof-mapping", {}, std::to_string(full_dof_count_), "Free DOF ownership must be unique."); } if (!IsStrictlyIncreasing(free_dofs_)) { return DofFailure("invalid-free-dof-mapping", {}, std::to_string(full_dof_count_), "Free DOFs must use stable increasing full-DOF order."); } if (HasAdjacentDuplicate(constrained_dofs_)) { return DofFailure("duplicate-dof-mapping", {}, std::to_string(full_dof_count_), "Constrained DOF ownership must be unique."); } if (!IsStrictlyIncreasing(constrained_dofs_)) { return DofFailure( "invalid-constrained-dof-mapping", {}, std::to_string(full_dof_count_), "Constrained DOFs must use stable increasing full-DOF order."); } std::vector ownership(full_dof_count_, 0U); for (std::size_t equation = 0U; equation < free_dofs_.size(); ++equation) { const std::size_t full_dof = free_dofs_[equation]; if (full_dof >= full_dof_count_) { return DofFailure("invalid-free-dof-mapping", {}, std::to_string(full_dof), "A free DOF is outside the full range."); } if (ownership[full_dof] != 0U) { return DofFailure("duplicate-dof-mapping", {}, std::to_string(full_dof), "Each full DOF must have one owner."); } if (free_equations_[full_dof] != equation) { return DofFailure( "invalid-equation-mapping", {}, std::to_string(full_dof), "Free equation numbering must match stable free-DOF order."); } ownership[full_dof] = 1U; } for (const std::size_t full_dof : constrained_dofs_) { if (full_dof >= full_dof_count_) { return DofFailure("invalid-constrained-dof-mapping", {}, std::to_string(full_dof), "A constrained DOF is outside the full range."); } if (ownership[full_dof] != 0U) { return DofFailure("duplicate-dof-mapping", {}, std::to_string(full_dof), "Each full DOF must have one owner."); } if (free_equations_[full_dof].has_value()) { return DofFailure("invalid-equation-mapping", {}, std::to_string(full_dof), "Constrained DOFs must be absent from free equations."); } ownership[full_dof] = 2U; } if (std::find(ownership.begin(), ownership.end(), 0U) != ownership.end()) { return DofFailure( "invalid-dof-partition", {}, std::to_string(full_dof_count_), "Free and constrained DOFs must partition the complete full range."); } for (const auto& scatter : element_scatters_) { if (scatter.empty()) { return DofFailure("invalid-element-dof-layout", {}, {}, "Stored element scatters must be nonempty."); } for (std::size_t position = 0U; position < scatter.size(); ++position) { const std::size_t full_dof = scatter[position]; if (full_dof >= full_dof_count_) { return DofFailure("invalid-element-dof-layout", {}, std::to_string(full_dof), "Stored element scatters must stay in range."); } if (std::find(scatter.begin(), scatter.begin() + static_cast(position), full_dof) != scatter.begin() + static_cast(position)) { return DofFailure("duplicate-element-dof", {}, std::to_string(full_dof), "Stored element scatters must remain unique."); } } } const SparsePattern expected_pattern = BuildSparsePattern(full_dof_count_, element_scatters_); if (sparse_pattern_.row_offsets != expected_pattern.row_offsets || sparse_pattern_.column_indices != expected_pattern.column_indices) { return DofFailure( "invalid-dof-sparse-pattern", {}, std::to_string(full_dof_count_), "The CSR pattern must exactly match the stored element scatters."); } return Status::Ok(); } DofManager::DofManager(const std::size_t full_dof_count, std::vector> free_equations, std::vector> 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)}, 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