feat(cpp-object-oriented-modular-refactoring): step 15 - generic-dof-manager
This commit is contained in:
@@ -12,6 +12,8 @@
|
||||
|
||||
namespace fesa {
|
||||
|
||||
class DofManagerTestAccess;
|
||||
|
||||
/// @brief Stores the stable structural CSR pattern.
|
||||
struct SparsePattern {
|
||||
std::vector<std::size_t> row_offsets;
|
||||
@@ -21,9 +23,22 @@ struct SparsePattern {
|
||||
/// @brief Owns full/free/constrained numbering, scatter maps, and CSR pattern.
|
||||
class DofManager {
|
||||
public:
|
||||
/// @brief Creates an empty candidate for atomic Build replacement.
|
||||
DofManager() = default;
|
||||
|
||||
/// @brief Creates every equation-space mapping for an active model.
|
||||
/// @note This compatibility entry point derives temporary semantic layouts;
|
||||
/// the procedure-owned runtime element view supersedes it in Step 20.
|
||||
static Result<DofManager> Create(const AnalysisModel& model);
|
||||
|
||||
/// @brief Builds mappings from runtime element layouts in supplied order.
|
||||
/// @param analysis_model Non-owning active model view that outlives this
|
||||
/// call.
|
||||
/// @param elements Runtime elements in stable active source order.
|
||||
/// @return Success after atomic replacement or a structured model failure.
|
||||
Status Build(const AnalysisModel& analysis_model,
|
||||
const ElementView& elements);
|
||||
|
||||
/// @brief Returns the full node-by-component DOF count.
|
||||
std::size_t FullDofCount() const noexcept;
|
||||
/// @brief Returns the free-equation count.
|
||||
@@ -34,11 +49,16 @@ class DofManager {
|
||||
std::size_t FullDof(EntityIndex node, DofComponent component) const;
|
||||
/// @brief Returns the free equation for a full DOF when unconstrained.
|
||||
std::optional<std::size_t> FreeEquation(std::size_t full_dof) const;
|
||||
/// @brief Maps one declared runtime layout to stable full DOFs.
|
||||
/// @return The declared node/component scatter or a layout failure.
|
||||
Result<std::vector<std::size_t>> ElementScatter(
|
||||
const ElementDofLayout& layout) const;
|
||||
/// @brief Returns a beam scatter in endpoint/component order.
|
||||
const std::array<std::size_t, 12>& ElementScatter(EntityIndex element) const;
|
||||
/// @note This compatibility wrapper delegates to the generic stored layout.
|
||||
std::array<std::size_t, 12> ElementScatter(EntityIndex element) const;
|
||||
/// @brief Returns a shell scatter in node/component order.
|
||||
const std::array<std::size_t, 24>& ShellElementScatter(
|
||||
EntityIndex element) const;
|
||||
/// @note This compatibility wrapper delegates to the generic stored layout.
|
||||
std::array<std::size_t, 24> ShellElementScatter(EntityIndex element) const;
|
||||
/// @brief Returns free full DOFs in stable increasing order.
|
||||
const std::vector<std::size_t>& FreeDofs() const noexcept;
|
||||
/// @brief Returns constrained full DOFs in stable increasing order.
|
||||
@@ -47,24 +67,30 @@ class DofManager {
|
||||
const Vector& PrescribedValues() const noexcept;
|
||||
/// @brief Returns the full-space structural CSR pattern.
|
||||
const SparsePattern& GetSparsePattern() const noexcept;
|
||||
/// @brief Validates the complete owner-issued equation and pattern mapping.
|
||||
Status ValidateInvariants() const;
|
||||
|
||||
private:
|
||||
friend class DofManagerTestAccess;
|
||||
|
||||
/// @brief Builds from copied layouts after the caller fixes their order.
|
||||
Status BuildLayouts(const AnalysisModel& analysis_model,
|
||||
const std::vector<ElementDofLayout>& layouts);
|
||||
|
||||
/// @brief Takes ownership of fully validated stable equation mappings.
|
||||
DofManager(std::size_t full_dof_count,
|
||||
std::vector<std::optional<std::size_t>> free_equations,
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters,
|
||||
std::vector<std::array<std::size_t, 24>> shell_element_scatters,
|
||||
std::vector<std::vector<std::size_t>> element_scatters,
|
||||
std::vector<std::size_t> free_dofs,
|
||||
std::vector<std::size_t> constrained_dofs,
|
||||
Vector prescribed_values, SparsePattern sparse_pattern);
|
||||
|
||||
std::size_t full_dof_count_;
|
||||
std::size_t full_dof_count_{0U};
|
||||
std::vector<std::optional<std::size_t>> free_equations_;
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters_;
|
||||
std::vector<std::array<std::size_t, 24>> shell_element_scatters_;
|
||||
std::vector<std::vector<std::size_t>> element_scatters_;
|
||||
std::vector<std::size_t> free_dofs_;
|
||||
std::vector<std::size_t> constrained_dofs_;
|
||||
Vector prescribed_values_;
|
||||
Vector prescribed_values_{0U};
|
||||
SparsePattern sparse_pattern_;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,79 +25,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location,
|
||||
{{Severity::kError, code, location, keyword, identity, message}});
|
||||
}
|
||||
|
||||
/// @brief Checks that equation-space indices preserve stable full-DOF order.
|
||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& 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 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<unsigned char> 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;
|
||||
}
|
||||
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<std::vector<EntityIndex>> ResolveTarget(
|
||||
const SourceTargetResolver& resolver, const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
@@ -207,8 +134,13 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
|
||||
"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 (dofs.FullDofCount() != expected_full_count) {
|
||||
return Result<Vector>::Failure(LoadFailure(
|
||||
"invalid-load-dimensions", {domain.SourcePath(), 0U}, "LOAD_ASSEMBLER",
|
||||
std::to_string(dofs.FullDofCount()),
|
||||
"The DofManager full dimension must match the active model."));
|
||||
}
|
||||
const Status dof_status = dofs.ValidateInvariants();
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<Vector>::Failure(dof_status);
|
||||
}
|
||||
@@ -296,7 +228,13 @@ Result<Vector> LoadAssembler::EffectiveFreeRhs(const Vector& full_load,
|
||||
const Vector& prescribed_values,
|
||||
const DofManager& dofs) {
|
||||
const SourceLocation location{{}, 0U};
|
||||
const Status dof_status = ValidateDofOrder(dofs, full_load.Size(), location);
|
||||
if (dofs.FullDofCount() != full_load.Size()) {
|
||||
return Result<Vector>::Failure(
|
||||
LoadFailure("invalid-load-dimensions", location, "LOAD_ASSEMBLER",
|
||||
std::to_string(dofs.FullDofCount()),
|
||||
"The DofManager full dimension must match the full load."));
|
||||
}
|
||||
const Status dof_status = dofs.ValidateInvariants();
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<Vector>::Failure(dof_status);
|
||||
}
|
||||
|
||||
@@ -22,71 +22,6 @@ Status ConstraintFailure(const std::string& code, const std::string& identity,
|
||||
message}});
|
||||
}
|
||||
|
||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& 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 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<unsigned char> 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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
/// @brief Extracts one partition without changing the supplied DOF order.
|
||||
Result<SparseMatrix> ExtractBlock(const SparseMatrix& full,
|
||||
const std::vector<std::size_t>& row_dofs,
|
||||
@@ -123,7 +58,7 @@ Result<SparseMatrix> ExtractBlock(const SparseMatrix& full,
|
||||
}
|
||||
|
||||
void RequireDofOrder(const DofManager& dofs) {
|
||||
if (!ValidateDofOrder(dofs).IsOk()) {
|
||||
if (!dofs.ValidateInvariants().IsOk()) {
|
||||
throw std::invalid_argument{
|
||||
"DofManager constraint dimensions or order are invalid."};
|
||||
}
|
||||
@@ -144,7 +79,7 @@ Result<PartitionedStiffness> EssentialConstraints::Partition(
|
||||
"Full stiffness must be square and match the DofManager full "
|
||||
"dimension."));
|
||||
}
|
||||
const Status dof_status = ValidateDofOrder(dofs);
|
||||
const Status dof_status = dofs.ValidateInvariants();
|
||||
if (!dof_status.IsOk()) {
|
||||
return Result<PartitionedStiffness>::Failure(dof_status);
|
||||
}
|
||||
|
||||
+300
-64
@@ -1,8 +1,13 @@
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
|
||||
@@ -11,6 +16,37 @@ 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<std::size_t>& 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<std::size_t>& values) {
|
||||
return std::adjacent_find(values.begin(), values.end()) != values.end();
|
||||
}
|
||||
|
||||
std::vector<DofComponent> FullNodeComponents() {
|
||||
return {DofComponent::kUx, DofComponent::kUy, DofComponent::kUz,
|
||||
DofComponent::kUrx, DofComponent::kUry, DofComponent::kUrz};
|
||||
}
|
||||
|
||||
std::vector<EntityIndex> ExpandBoundaryTarget(
|
||||
const SourceTargetResolver& resolver, const BoundaryCondition& boundary) {
|
||||
auto resolved =
|
||||
@@ -26,9 +62,8 @@ std::vector<EntityIndex> ExpandBoundaryTarget(
|
||||
return indices;
|
||||
}
|
||||
|
||||
template <std::size_t scatter_size>
|
||||
void AppendScatter(std::vector<std::vector<std::size_t>>& columns_by_row,
|
||||
const std::array<std::size_t, scatter_size>& scatter) {
|
||||
const std::vector<std::size_t>& scatter) {
|
||||
for (const std::size_t row : scatter) {
|
||||
auto& columns = columns_by_row[row];
|
||||
columns.insert(columns.end(), scatter.begin(), scatter.end());
|
||||
@@ -37,15 +72,10 @@ void AppendScatter(std::vector<std::vector<std::size_t>>& columns_by_row,
|
||||
|
||||
/// @brief Builds sorted unique CSR columns by deterministic scatter traversal.
|
||||
SparsePattern BuildSparsePattern(
|
||||
std::size_t full_dof_count, const std::vector<EntityIndex>& active_elements,
|
||||
const std::vector<std::array<std::size_t, 12>>& element_scatters,
|
||||
const std::vector<std::array<std::size_t, 24>>& shell_element_scatters) {
|
||||
const std::size_t full_dof_count,
|
||||
const std::vector<std::vector<std::size_t>>& element_scatters) {
|
||||
std::vector<std::vector<std::size_t>> 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) {
|
||||
for (const auto& scatter : element_scatters) {
|
||||
AppendScatter(columns_by_row, scatter);
|
||||
}
|
||||
|
||||
@@ -63,17 +93,94 @@ SparsePattern BuildSparsePattern(
|
||||
return pattern;
|
||||
}
|
||||
|
||||
template <std::size_t kSize>
|
||||
std::array<std::size_t, kSize> FixedScatter(
|
||||
const std::vector<std::size_t>& scatter) {
|
||||
if (scatter.size() != kSize) {
|
||||
throw std::out_of_range{
|
||||
"Stored element scatter does not match the compatibility shape."};
|
||||
}
|
||||
std::array<std::size_t, kSize> fixed{};
|
||||
std::copy(scatter.begin(), scatter.end(), fixed.begin());
|
||||
return fixed;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||
const Domain& domain = model.GetDomain();
|
||||
std::vector<ElementDofLayout> layouts;
|
||||
layouts.reserve(model.ActiveElements().size());
|
||||
for (const EntityIndex element_index : model.ActiveElements()) {
|
||||
if (element_index >= domain.Elements().Size()) {
|
||||
return Result<DofManager>::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<DofManager>::Failure(status);
|
||||
}
|
||||
return Result<DofManager>::Success(std::move(dofs));
|
||||
}
|
||||
|
||||
Status DofManager::Build(const AnalysisModel& analysis_model,
|
||||
const ElementView& elements) {
|
||||
std::vector<ElementDofLayout> 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<ElementDofLayout>& layouts) {
|
||||
const Domain& domain = analysis_model.GetDomain();
|
||||
if (domain.Nodes().size() >
|
||||
(std::numeric_limits<std::size_t>::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<std::optional<double>> prescribed_by_full_dof(full_count);
|
||||
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
|
||||
const auto& boundary = model.Step().boundaries.at(boundary_index);
|
||||
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;
|
||||
@@ -83,12 +190,12 @@ Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||
static_cast<std::size_t>(component - 1);
|
||||
auto& prescribed = prescribed_by_full_dof[full_dof];
|
||||
if (prescribed && *prescribed != boundary.value) {
|
||||
return Result<DofManager>::Failure(Status::Failure(
|
||||
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."}}));
|
||||
"node/DOF."}});
|
||||
}
|
||||
prescribed = boundary.value;
|
||||
}
|
||||
@@ -119,44 +226,30 @@ Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||
prescribed_values[index] = constrained_values[index];
|
||||
}
|
||||
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters(
|
||||
domain.BeamElements().Size());
|
||||
for (const EntityIndex element_index : model.ActiveBeamElements()) {
|
||||
const auto& element = domain.BeamElements().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;
|
||||
}
|
||||
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_);
|
||||
|
||||
std::vector<std::array<std::size_t, 24>> 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;
|
||||
const Status invariant_status = candidate.ValidateInvariants();
|
||||
if (!invariant_status.IsOk()) {
|
||||
return invariant_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
auto pattern = BuildSparsePattern(full_count, model.ActiveBeamElements(),
|
||||
element_scatters, shell_element_scatters);
|
||||
return Result<DofManager>::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)});
|
||||
*this = std::move(candidate);
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
std::size_t DofManager::FullDofCount() const noexcept {
|
||||
@@ -171,8 +264,8 @@ std::size_t DofManager::ConstrainedDofCount() const noexcept {
|
||||
return constrained_dofs_.size();
|
||||
}
|
||||
|
||||
std::size_t DofManager::FullDof(EntityIndex node,
|
||||
DofComponent component) const {
|
||||
std::size_t DofManager::FullDof(const EntityIndex node,
|
||||
const DofComponent component) const {
|
||||
const std::size_t component_index = static_cast<std::size_t>(component);
|
||||
if (node >= full_dof_count_ / kDofsPerNode ||
|
||||
component_index >= kDofsPerNode) {
|
||||
@@ -182,18 +275,57 @@ std::size_t DofManager::FullDof(EntityIndex node,
|
||||
}
|
||||
|
||||
std::optional<std::size_t> DofManager::FreeEquation(
|
||||
std::size_t full_dof) const {
|
||||
const std::size_t full_dof) const {
|
||||
return free_equations_.at(full_dof);
|
||||
}
|
||||
|
||||
const std::array<std::size_t, 12>& DofManager::ElementScatter(
|
||||
EntityIndex element) const {
|
||||
return element_scatters_.at(element);
|
||||
Result<std::vector<std::size_t>> DofManager::ElementScatter(
|
||||
const ElementDofLayout& layout) const {
|
||||
if (layout.node_indices.empty() || layout.components_per_node.empty() ||
|
||||
layout.node_indices.size() > (std::numeric_limits<std::size_t>::max)() /
|
||||
layout.components_per_node.size()) {
|
||||
return Result<std::vector<std::size_t>>::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<std::size_t> 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<std::size_t>(component);
|
||||
if (node >= full_dof_count_ / kDofsPerNode ||
|
||||
component_index >= kDofsPerNode) {
|
||||
return Result<std::vector<std::size_t>>::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<std::size_t>(node) * kDofsPerNode + component_index;
|
||||
if (std::find(scatter.begin(), scatter.end(), full_dof) !=
|
||||
scatter.end()) {
|
||||
return Result<std::vector<std::size_t>>::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<std::vector<std::size_t>>::Success(std::move(scatter));
|
||||
}
|
||||
|
||||
const std::array<std::size_t, 24>& DofManager::ShellElementScatter(
|
||||
EntityIndex element) const {
|
||||
return shell_element_scatters_.at(element);
|
||||
std::array<std::size_t, 12> DofManager::ElementScatter(
|
||||
const EntityIndex element) const {
|
||||
return FixedScatter<12U>(element_scatters_.at(element));
|
||||
}
|
||||
|
||||
std::array<std::size_t, 24> DofManager::ShellElementScatter(
|
||||
const EntityIndex element) const {
|
||||
return FixedScatter<24U>(element_scatters_.at(element));
|
||||
}
|
||||
|
||||
const std::vector<std::size_t>& DofManager::FreeDofs() const noexcept {
|
||||
@@ -212,18 +344,122 @@ const SparsePattern& DofManager::GetSparsePattern() const noexcept {
|
||||
return sparse_pattern_;
|
||||
}
|
||||
|
||||
DofManager::DofManager(
|
||||
std::size_t full_dof_count,
|
||||
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<unsigned char> 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<std::ptrdiff_t>(position),
|
||||
full_dof) !=
|
||||
scatter.begin() + static_cast<std::ptrdiff_t>(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<std::optional<std::size_t>> free_equations,
|
||||
std::vector<std::array<std::size_t, 12>> element_scatters,
|
||||
std::vector<std::array<std::size_t, 24>> shell_element_scatters,
|
||||
std::vector<std::vector<std::size_t>> element_scatters,
|
||||
std::vector<std::size_t> free_dofs,
|
||||
std::vector<std::size_t> constrained_dofs, Vector prescribed_values,
|
||||
SparsePattern sparse_pattern)
|
||||
std::vector<std::size_t> 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)},
|
||||
|
||||
@@ -4,13 +4,93 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
class DofManagerTestAccess {
|
||||
public:
|
||||
static void DuplicateOwnership(DofManager& dofs) {
|
||||
dofs.constrained_dofs_[3U] = dofs.free_dofs_[7U];
|
||||
}
|
||||
|
||||
static void RemoveFullMapping(DofManager& dofs) {
|
||||
dofs.free_equations_.pop_back();
|
||||
}
|
||||
|
||||
static void ReverseFreeMapping(DofManager& dofs) {
|
||||
std::swap(dofs.free_dofs_[0U], dofs.free_dofs_[1U]);
|
||||
}
|
||||
|
||||
static void ReverseConstrainedMapping(DofManager& dofs) {
|
||||
std::swap(dofs.constrained_dofs_[0U], dofs.constrained_dofs_[1U]);
|
||||
}
|
||||
|
||||
static void CorruptEquationMapping(DofManager& dofs) {
|
||||
dofs.free_equations_[dofs.free_dofs_[0U]] = dofs.free_dofs_.size();
|
||||
}
|
||||
|
||||
static void RemoveSparsePatternEntry(DofManager& dofs) {
|
||||
const std::size_t position = dofs.sparse_pattern_.row_offsets[1U] - 1U;
|
||||
dofs.sparse_pattern_.column_indices.erase(
|
||||
dofs.sparse_pattern_.column_indices.begin() +
|
||||
static_cast<std::ptrdiff_t>(position));
|
||||
for (std::size_t row = 1U; row < dofs.sparse_pattern_.row_offsets.size();
|
||||
++row) {
|
||||
--dofs.sparse_pattern_.row_offsets[row];
|
||||
}
|
||||
}
|
||||
|
||||
static void AddSparsePatternEntry(DofManager& dofs) {
|
||||
const std::size_t position = dofs.sparse_pattern_.row_offsets[1U];
|
||||
dofs.sparse_pattern_.column_indices.insert(
|
||||
dofs.sparse_pattern_.column_indices.begin() +
|
||||
static_cast<std::ptrdiff_t>(position),
|
||||
12U);
|
||||
for (std::size_t row = 1U; row < dofs.sparse_pattern_.row_offsets.size();
|
||||
++row) {
|
||||
++dofs.sparse_pattern_.row_offsets[row];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
|
||||
namespace {
|
||||
|
||||
class GenericLayoutElement final : public fesa::Element {
|
||||
public:
|
||||
explicit GenericLayoutElement(fesa::ElementDofLayout layout)
|
||||
: layout_{std::move(layout)} {}
|
||||
|
||||
const fesa::ElementDofLayout& DofLayout() const noexcept override {
|
||||
return layout_;
|
||||
}
|
||||
|
||||
fesa::Result<fesa::ElementStiffnessContribution> ComputeStiffness()
|
||||
const override {
|
||||
const std::size_t local_dof_count =
|
||||
layout_.node_indices.size() * layout_.components_per_node.size();
|
||||
return fesa::Result<fesa::ElementStiffnessContribution>::Success(
|
||||
{layout_, fesa::Matrix{local_dof_count, local_dof_count}});
|
||||
}
|
||||
|
||||
fesa::Result<fesa::ElementResultBundle> Recover(
|
||||
const fesa::Vector&) const override {
|
||||
return fesa::Result<fesa::ElementResultBundle>::Success(
|
||||
{layout_.source_id, fesa::BeamElementResultRows{}});
|
||||
}
|
||||
|
||||
private:
|
||||
fesa::ElementDofLayout layout_;
|
||||
};
|
||||
|
||||
fesa::ModelDefinition MakeDefinition() {
|
||||
const std::filesystem::path source{"models/dof-manager.inp"};
|
||||
fesa::ModelDefinition definition{};
|
||||
@@ -83,8 +163,102 @@ std::vector<std::size_t> RowColumns(const fesa::SparsePattern& pattern,
|
||||
pattern.column_indices.begin() + pattern.row_offsets[row + 1U]};
|
||||
}
|
||||
|
||||
void ExpectInvariantFailure(const fesa::DofManager& dofs,
|
||||
const std::string& code) {
|
||||
const fesa::Status status = dofs.ValidateInvariants();
|
||||
ASSERT_FALSE(status.IsOk());
|
||||
EXPECT_EQ(status.Category(), fesa::FailureCategory::kModel);
|
||||
ASSERT_EQ(status.Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(status.Diagnostics()[0U].code, code);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// C-DOF-001
|
||||
TEST(DofManager, BuildsGenericRuntimeLayoutsInDeclaredSourceOrder) {
|
||||
auto domain = fesa::Domain::Create(MakeDefinition());
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
auto model = fesa::AnalysisModel::Create(domain.Value());
|
||||
ASSERT_TRUE(model.HasValue());
|
||||
|
||||
GenericLayoutElement first{fesa::ElementDofLayout{
|
||||
{"Beam-1", 100, "100"},
|
||||
{2U, 0U},
|
||||
{fesa::DofComponent::kUz, fesa::DofComponent::kUx}}};
|
||||
GenericLayoutElement second{
|
||||
fesa::ElementDofLayout{{"Beam-1", 200, "200"},
|
||||
{1U},
|
||||
{fesa::DofComponent::kUrz, fesa::DofComponent::kUy,
|
||||
fesa::DofComponent::kUrx}}};
|
||||
const fesa::ElementView elements{std::cref(first), std::cref(second)};
|
||||
|
||||
fesa::DofManager dofs;
|
||||
ASSERT_TRUE(dofs.Build(model.Value(), elements).IsOk());
|
||||
ASSERT_TRUE(dofs.ValidateInvariants().IsOk());
|
||||
|
||||
auto first_scatter = dofs.ElementScatter(first.DofLayout());
|
||||
auto second_scatter = dofs.ElementScatter(second.DofLayout());
|
||||
ASSERT_TRUE(first_scatter.HasValue());
|
||||
ASSERT_TRUE(second_scatter.HasValue());
|
||||
EXPECT_EQ(first_scatter.Value(),
|
||||
(std::vector<std::size_t>{14U, 12U, 2U, 0U}));
|
||||
EXPECT_EQ(second_scatter.Value(), (std::vector<std::size_t>{11U, 7U, 9U}));
|
||||
|
||||
const auto& pattern = dofs.GetSparsePattern();
|
||||
EXPECT_EQ(RowColumns(pattern, 14U),
|
||||
(std::vector<std::size_t>{0U, 2U, 12U, 14U}));
|
||||
EXPECT_EQ(RowColumns(pattern, 7U), (std::vector<std::size_t>{7U, 9U, 11U}));
|
||||
|
||||
const auto expected_free_dofs = dofs.FreeDofs();
|
||||
const auto expected_constrained_dofs = dofs.ConstrainedDofs();
|
||||
const auto expected_row_offsets = pattern.row_offsets;
|
||||
const auto expected_columns = pattern.column_indices;
|
||||
const fesa::ElementView reversed_elements{std::cref(second),
|
||||
std::cref(first)};
|
||||
const fesa::Status reversed_status =
|
||||
dofs.Build(model.Value(), reversed_elements);
|
||||
ASSERT_FALSE(reversed_status.IsOk());
|
||||
ASSERT_EQ(reversed_status.Diagnostics().size(), 1U);
|
||||
EXPECT_EQ(reversed_status.Diagnostics()[0U].code,
|
||||
"invalid-element-layout-order");
|
||||
EXPECT_EQ(dofs.FreeDofs(), expected_free_dofs);
|
||||
EXPECT_EQ(dofs.ConstrainedDofs(), expected_constrained_dofs);
|
||||
EXPECT_EQ(dofs.GetSparsePattern().row_offsets, expected_row_offsets);
|
||||
EXPECT_EQ(dofs.GetSparsePattern().column_indices, expected_columns);
|
||||
}
|
||||
|
||||
TEST(DofManager, RejectsEveryCorruptedOwnerMappingInvariant) {
|
||||
const auto fixture = MakeDofFixture();
|
||||
|
||||
auto duplicate = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::DuplicateOwnership(duplicate);
|
||||
ExpectInvariantFailure(duplicate, "duplicate-dof-mapping");
|
||||
|
||||
auto full = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::RemoveFullMapping(full);
|
||||
ExpectInvariantFailure(full, "invalid-dof-dimensions");
|
||||
|
||||
auto free = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::ReverseFreeMapping(free);
|
||||
ExpectInvariantFailure(free, "invalid-free-dof-mapping");
|
||||
|
||||
auto constrained = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::ReverseConstrainedMapping(constrained);
|
||||
ExpectInvariantFailure(constrained, "invalid-constrained-dof-mapping");
|
||||
|
||||
auto equation = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::CorruptEquationMapping(equation);
|
||||
ExpectInvariantFailure(equation, "invalid-equation-mapping");
|
||||
|
||||
auto missing_pattern_entry = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::RemoveSparsePatternEntry(missing_pattern_entry);
|
||||
ExpectInvariantFailure(missing_pattern_entry, "invalid-dof-sparse-pattern");
|
||||
|
||||
auto extra_pattern_entry = fixture.dofs;
|
||||
fesa::DofManagerTestAccess::AddSparsePatternEntry(extra_pattern_entry);
|
||||
ExpectInvariantFailure(extra_pattern_entry, "invalid-dof-sparse-pattern");
|
||||
}
|
||||
|
||||
TEST(DofManager, NumbersSixDofsAndFreeEquationsStably) {
|
||||
const auto fixture = MakeDofFixture();
|
||||
const auto& dofs = fixture.dofs;
|
||||
|
||||
Reference in New Issue
Block a user