feat(linear-static-3d-euler-beam): step 14 - dof-manager

This commit is contained in:
KOKO\Mimi
2026-08-09 17:10:34 +09:00
parent a362d9938a
commit ac0e6b566e
6 changed files with 542 additions and 0 deletions
@@ -405,3 +405,57 @@
- handoff: the exact AnalysisModel ledger API, sole-step reference, and stable
active element/material/section/boundary/load IDs are available to Step 14
`DofManager` without copying or mutating Domain entities.
## Step 14 — dof-manager
- task_id: `TASK-14`
- status: `completed`
- changed_files: `include/fesa/fem/dof_manager.hpp`,
`src/fesa/fem/dof_manager.cpp`, `tests/unit/fem/dof_manager_test.cpp`,
`src/fesa/CMakeLists.txt`, `tests/CMakeLists.txt`,
`docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md`,
`phases/linear-static-3d-euler-beam/index.json`,
`.superpowers/sdd/linear-static-3d-euler-beam/task-14-report.md`
- requirement_ids: `FESA-REQ-LS3DEB-003`, `FESA-REQ-LS3DEB-007`,
`FESA-REQ-LS3DEB-011`, `FESA-REQ-LS3DEB-022`,
`FESA-REQ-LS3DEB-034`
- test_ids: `T14-DOF-001`, `T14-DOF-002`, `T14-DOF-003`,
`T14-DOF-004`
| stage | exact command | exit_code | expected_or_observed_result | evidence_tail |
| --- | --- | ---: | --- | --- |
| RED-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 1 | Exactly four planned tests are registered before production and fail for the missing DofManager API | MSVC C1083 reported missing `fesa/fem/dof_manager.hpp` from `dof_manager_test.cpp` after successful CMake regeneration |
| RED-test | `ctest --test-dir .harness/build -C Debug -R DofManager --output-on-failure` | 0 | No focused test is runnable because the test executable cannot rebuild | CTest reported `No tests were found` after the implementation-owned compile RED |
| GREEN-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 0 | Minimal DofManager, its four tests, solver library, and unit executable compile and link | `dof_manager.cpp`, `dof_manager_test.cpp`, `fesa_solver.lib`, and `fesa_unit_tests.exe` built without a FESA warning under `/W4 /WX` |
| GREEN-test | `ctest --test-dir .harness/build -C Debug -R DofManager --output-on-failure` | 0 | Numbering, constraints, scatter/pattern, and mapping-owned round-trip pass | 4/4 exact `DofManager` tests passed |
| VERIFY-configure | `cmake -S . -B .harness/build -A x64 -DFESA_GTEST_SOURCE_DIR=C:/git/googletest "-DMKL_DIR=C:/Program Files (x86)/Intel/oneAPI/mkl/2026.1/lib/cmake/mkl" "-DTBB_DIR=C:/Program Files (x86)/Intel/oneAPI/tbb/2023.1/lib/cmake/tbb" "-DHDF5_DIR=C:/Program Files/HDF_Group/HDF5/2.1.1/cmake"` | 0 | Approved explicit-dependency MSVC x64 build tree generates | Windows SDK and oneMKL 2026.1 resolved; configure and generate completed |
| VERIFY-build | `cmake --build .harness/build --config Debug` | 0 | Full Debug build passes without a new FESA warning | `fesa_solver.lib` and `fesa_unit_tests.exe` built under `/W4 /WX` |
| VERIFY-targeted | `ctest --test-dir .harness/build -C Debug -R DofManager --output-on-failure` | 0 | Focused Step 14 suite remains green | 4/4 exact `DofManager` tests passed |
| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all four exact DofManager names | 26 tests discovered, including 4 `DofManager` tests, with feature/unit labels |
| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 26/26 tests passed |
| VERIFY-contract-scans | Backend public-header, upward-FEM dependency, model/analysis equation-state, out-of-scope feature, unledgered API, exact-test-count, and CMake-registration scans using fail-on-match `rg` wrappers | 0 | DofManager remains the sole equation/scatter/pattern owner and implements only the exact public ledger | backend leaks 0; upward dependencies 0; model/analysis equation state 0; penalty/MPC/RBE/numeric sparse storage 0; unledgered APIs 0; tests 4; registrations 1/1 |
| VERIFY-diff | `git diff --check` plus trailing-whitespace scan over the three new files | 0 | Tracked and untracked Step 14 files have no whitespace errors | Diff check exit 0; new-file trailing whitespace matches 0 |
| VERIFY-reference | `git diff --exit-code -- reference/`; `git status --short -- reference/` | 0 | Approved legacy reference artifacts remain read-only and unchanged | Reference diff exit 0 and reference status empty |
- contract_checks: full DOFs follow stable node index and exact component order
`[UX,UY,UZ,URX,URY,URZ]`; free equations and constrained DOFs are ascending
full-index mappings. Case-insensitive node-set and direct source-label targets
expand deterministically; equal overlap is retained once, while different
values return `conflicting-boundary-condition` at the conflicting row's
source location. `prescribedValues()` has the same size/order as
`constrainedDofs()` and preserves zero and nonzero values.
- contract_checks: each active B33 definition has one 12-entry scatter in
endpoint/component order. The full-space CSR pattern has 19 row offsets and
252 sorted-unique structural columns for the two-element chain fixture.
Stable free/constrained maps reconstruct the full vector without adding an
API absent from the exact ledger. Node and element records remain unchanged
and contain no equation IDs; no penalty, MPC, RBE, or sparse numeric storage
was added.
- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`,
`.harness/build/tests/Debug/fesa_unit_tests.exe`,
`.harness/build/step14-ctest-discovery.json`
- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0
- handoff: backend-free `DofComponent`, `SparsePattern`, and the exact
`DofManager` ledger API provide deterministic full/free/constrained maps,
prescribed `dc`, active-element scatter, and full structural CSR pattern to
Step 15 and later assembly/constraint tasks.
+65
View File
@@ -0,0 +1,65 @@
#pragma once
#include "fesa/analysis/analysis_model.hpp"
#include "fesa/math/vector.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
namespace fesa {
enum class DofComponent : std::uint8_t {
ux,
uy,
uz,
urx,
ury,
urz
};
struct SparsePattern {
std::vector<std::size_t> rowOffsets;
std::vector<std::size_t> columnIndices;
};
// Owns every equation-space mapping so semantic model objects remain free of
// analysis-specific equation IDs.
class DofManager {
public:
static Result<DofManager> create(const AnalysisModel& model);
std::size_t fullDofCount() const noexcept;
std::size_t freeDofCount() const noexcept;
std::size_t constrainedDofCount() const noexcept;
std::size_t fullDof(EntityIndex node, DofComponent component) const;
std::optional<std::size_t> freeEquation(std::size_t fullDof) const;
const std::array<std::size_t, 12>& elementScatter(
EntityIndex element) const;
const std::vector<std::size_t>& freeDofs() const noexcept;
const std::vector<std::size_t>& constrainedDofs() const noexcept;
const Vector& prescribedValues() const noexcept;
const SparsePattern& sparsePattern() const noexcept;
private:
DofManager(
std::size_t fullDofCount,
std::vector<std::optional<std::size_t>> freeEquations,
std::vector<std::array<std::size_t, 12>> elementScatters,
std::vector<std::size_t> freeDofs,
std::vector<std::size_t> constrainedDofs,
Vector prescribedValues,
SparsePattern sparsePattern);
std::size_t fullDofCount_;
std::vector<std::optional<std::size_t>> freeEquations_;
std::vector<std::array<std::size_t, 12>> elementScatters_;
std::vector<std::size_t> freeDofs_;
std::vector<std::size_t> constrainedDofs_;
Vector prescribedValues_;
SparsePattern sparsePattern_;
};
} // namespace fesa
+1
View File
@@ -5,6 +5,7 @@ add_library(
build_info.cpp
core/diagnostic.cpp
core/status.cpp
fem/dof_manager.cpp
io/abaqus/domain_mapper.cpp
io/abaqus/input_reader.cpp
math/matrix.cpp
+230
View File
@@ -0,0 +1,230 @@
#include "fesa/fem/dof_manager.hpp"
#include <algorithm>
#include <charconv>
#include <stdexcept>
#include <string>
#include <system_error>
#include <utility>
namespace fesa {
namespace {
constexpr std::size_t dofsPerNode = 6U;
char asciiLower(char value) {
if (value >= 'A' && value <= 'Z') {
return static_cast<char>(value + ('a' - 'A'));
}
return value;
}
bool equalName(const std::string& left, const std::string& right) {
return left.size() == right.size() &&
std::equal(
left.begin(), left.end(), right.begin(),
[](char leftValue, char rightValue) {
return asciiLower(leftValue) == asciiLower(rightValue);
});
}
bool tryPositiveInteger(const std::string& text, std::int64_t& value) {
const char* const first = text.data();
const char* const last = first + text.size();
const auto parsed = std::from_chars(first, last, value);
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
}
std::vector<EntityIndex> expandBoundaryTarget(
const Domain& domain, const BoundaryCondition& boundary) {
for (const auto& set : domain.nodeSets()) {
if (equalName(set.name, boundary.target)) {
return set.nodeIndices;
}
}
std::int64_t sourceLabel = 0;
if (tryPositiveInteger(boundary.target, sourceLabel)) {
for (std::size_t node = 0U; node < domain.nodes().size(); ++node) {
if (domain.nodes()[node].sourceId.sourceLabel == sourceLabel) {
return {static_cast<EntityIndex>(node)};
}
}
}
return {};
}
SparsePattern buildSparsePattern(
std::size_t fullDofCount,
const std::vector<EntityIndex>& activeElements,
const std::vector<std::array<std::size_t, 12>>& elementScatters) {
std::vector<std::vector<std::size_t>> columnsByRow(fullDofCount);
for (const EntityIndex element : activeElements) {
const auto& scatter = elementScatters.at(element);
for (const std::size_t row : scatter) {
auto& columns = columnsByRow[row];
columns.insert(columns.end(), scatter.begin(), scatter.end());
}
}
SparsePattern pattern;
pattern.rowOffsets.reserve(fullDofCount + 1U);
pattern.rowOffsets.push_back(0U);
for (auto& columns : columnsByRow) {
// Stable CSR structure is independent of element traversal duplicates.
std::sort(columns.begin(), columns.end());
columns.erase(std::unique(columns.begin(), columns.end()), columns.end());
pattern.columnIndices.insert(
pattern.columnIndices.end(), columns.begin(), columns.end());
pattern.rowOffsets.push_back(pattern.columnIndices.size());
}
return pattern;
}
} // namespace
Result<DofManager> DofManager::create(const AnalysisModel& model) {
const Domain& domain = model.domain();
const std::size_t fullCount = domain.nodes().size() * dofsPerNode;
std::vector<std::optional<double>> prescribedByFullDof(fullCount);
for (const EntityIndex boundaryIndex : model.activeBoundaryConditions()) {
const auto& boundary = model.step().boundaries.at(boundaryIndex);
const auto target = expandBoundaryTarget(domain, boundary);
for (const EntityIndex node : target) {
for (int component = boundary.firstDof;
component <= boundary.lastDof;
++component) {
const std::size_t fullDof =
static_cast<std::size_t>(node) * dofsPerNode +
static_cast<std::size_t>(component - 1);
auto& prescribed = prescribedByFullDof[fullDof];
if (prescribed && *prescribed != boundary.value) {
return Result<DofManager>::failure(Status::failure(
FailureCategory::input,
{{Severity::error,
"conflicting-boundary-condition",
boundary.location,
"BOUNDARY",
boundary.target,
"Expanded boundary rows prescribe different values to one node/DOF."}}));
}
prescribed = boundary.value;
}
}
}
std::vector<std::size_t> freeDofs;
std::vector<std::size_t> constrainedDofs;
std::vector<double> constrainedValues;
std::vector<std::optional<std::size_t>> freeEquations(fullCount);
freeDofs.reserve(fullCount);
constrainedDofs.reserve(fullCount);
constrainedValues.reserve(fullCount);
// A full-DOF scan fixes free equations, constrained DOFs, and dc in the
// same stable order regardless of boundary declaration overlap.
for (std::size_t fullDof = 0U; fullDof < fullCount; ++fullDof) {
if (prescribedByFullDof[fullDof]) {
constrainedDofs.push_back(fullDof);
constrainedValues.push_back(*prescribedByFullDof[fullDof]);
} else {
freeEquations[fullDof] = freeDofs.size();
freeDofs.push_back(fullDof);
}
}
Vector prescribedValues{constrainedValues.size()};
for (std::size_t index = 0U; index < constrainedValues.size(); ++index) {
prescribedValues[index] = constrainedValues[index];
}
std::vector<std::array<std::size_t, 12>> elementScatters(
domain.elements().size());
for (const EntityIndex elementIndex : model.activeElements()) {
const auto& element = domain.elements().at(elementIndex);
auto& scatter = elementScatters.at(elementIndex);
for (std::size_t endpoint = 0U; endpoint < element.nodeIndices.size(); ++endpoint) {
const std::size_t node = element.nodeIndices[endpoint];
for (std::size_t component = 0U; component < dofsPerNode; ++component) {
scatter[endpoint * dofsPerNode + component] =
node * dofsPerNode + component;
}
}
}
auto pattern = buildSparsePattern(
fullCount, model.activeElements(), elementScatters);
return Result<DofManager>::success(DofManager{
fullCount,
std::move(freeEquations),
std::move(elementScatters),
std::move(freeDofs),
std::move(constrainedDofs),
std::move(prescribedValues),
std::move(pattern)});
}
std::size_t DofManager::fullDofCount() const noexcept {
return fullDofCount_;
}
std::size_t DofManager::freeDofCount() const noexcept {
return freeDofs_.size();
}
std::size_t DofManager::constrainedDofCount() const noexcept {
return constrainedDofs_.size();
}
std::size_t DofManager::fullDof(
EntityIndex node, DofComponent component) const {
const std::size_t componentIndex = static_cast<std::size_t>(component);
if (node >= fullDofCount_ / dofsPerNode || componentIndex >= dofsPerNode) {
throw std::out_of_range{"Node or DOF component is out of range."};
}
return static_cast<std::size_t>(node) * dofsPerNode + componentIndex;
}
std::optional<std::size_t> DofManager::freeEquation(
std::size_t fullDof) const {
return freeEquations_.at(fullDof);
}
const std::array<std::size_t, 12>& DofManager::elementScatter(
EntityIndex element) const {
return elementScatters_.at(element);
}
const std::vector<std::size_t>& DofManager::freeDofs() const noexcept {
return freeDofs_;
}
const std::vector<std::size_t>& DofManager::constrainedDofs() const noexcept {
return constrainedDofs_;
}
const Vector& DofManager::prescribedValues() const noexcept {
return prescribedValues_;
}
const SparsePattern& DofManager::sparsePattern() const noexcept {
return sparsePattern_;
}
DofManager::DofManager(
std::size_t fullDofCount,
std::vector<std::optional<std::size_t>> freeEquations,
std::vector<std::array<std::size_t, 12>> elementScatters,
std::vector<std::size_t> freeDofs,
std::vector<std::size_t> constrainedDofs,
Vector prescribedValues,
SparsePattern sparsePattern)
: fullDofCount_{fullDofCount},
freeEquations_{std::move(freeEquations)},
elementScatters_{std::move(elementScatters)},
freeDofs_{std::move(freeDofs)},
constrainedDofs_{std::move(constrainedDofs)},
prescribedValues_{std::move(prescribedValues)},
sparsePattern_{std::move(sparsePattern)} {}
} // namespace fesa
+1
View File
@@ -7,6 +7,7 @@ add_executable(
unit/core/diagnostic_test.cpp
unit/core/source_identity_test.cpp
unit/core/status_test.cpp
unit/fem/dof_manager_test.cpp
unit/math/matrix_test.cpp
unit/math/vector_test.cpp
unit/io/abaqus/domain_mapper_test.cpp
+191
View File
@@ -0,0 +1,191 @@
#include "fesa/fem/dof_manager.hpp"
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <filesystem>
#include <optional>
#include <utility>
#include <vector>
namespace {
fesa::ModelDefinition makeDefinition() {
const std::filesystem::path source{"models/dof-manager.inp"};
fesa::ModelDefinition definition{};
definition.sourcePath = source;
definition.sourceContentIdentity = "fnv1a64:0123456789abcdef";
definition.nodes = {
{{"Beam-1", 10, "10"}, {0.0, 0.0, 0.0}, {source, 10U}},
{{"Beam-1", 20, "20"}, {1.0, 0.0, 0.0}, {source, 11U}},
{{"Beam-1", 30, "30"}, {2.0, 0.0, 0.0}, {source, 12U}}};
definition.materials = {
{"Material", 1000.0, 0.25, {source, 20U}}};
definition.sections = {{
"Section", 1.0, 1.0, 0.0, 1.0, 1.0,
{0.0, 1.0, 0.0}, {}, {source, 30U}}};
definition.elements = {
{{"Beam-1", 100, "100"}, {0U, 1U}, 0U, 0U, {source, 40U}},
{{"Beam-1", 200, "200"}, {1U, 2U}, 0U, 0U, {source, 41U}}};
definition.nodeSets = {
{"Root", std::optional<std::string>{"Beam-1"}, {0U}, {source, 50U}},
{"Ends", std::optional<std::string>{"Beam-1"}, {0U, 2U}, {source, 51U}}};
definition.steps = {{
"Step-1",
{{"Root", 1, 2, 0.0, {source, 60U}},
{"ends", 3, 3, 0.25, {source, 61U}},
{"20", 6, 6, -0.5, {source, 62U}},
{"Root", 1, 1, 0.0, {source, 63U}}},
{},
0.1,
1.0,
0.01,
1.0,
{source, 59U}}};
return definition;
}
struct DofFixture {
fesa::DofManager dofs;
};
DofFixture makeDofFixture(fesa::ModelDefinition definition = makeDefinition()) {
auto domain = fesa::Domain::create(std::move(definition));
EXPECT_TRUE(domain.hasValue());
auto model = fesa::AnalysisModel::create(domain.value());
EXPECT_TRUE(model.hasValue());
auto dofs = fesa::DofManager::create(model.value());
EXPECT_TRUE(dofs.hasValue());
return {std::move(dofs.value())};
}
std::vector<std::size_t> rowColumns(
const fesa::SparsePattern& pattern, std::size_t row) {
return {
pattern.columnIndices.begin() + pattern.rowOffsets[row],
pattern.columnIndices.begin() + pattern.rowOffsets[row + 1U]};
}
} // namespace
TEST(DofManager, NumbersSixDofsAndFreeEquationsStably) {
const auto fixture = makeDofFixture();
const auto& dofs = fixture.dofs;
EXPECT_EQ(dofs.fullDofCount(), 18U);
EXPECT_EQ(dofs.freeDofCount(), 13U);
EXPECT_EQ(dofs.constrainedDofCount(), 5U);
EXPECT_EQ(dofs.fullDof(0U, fesa::DofComponent::ux), 0U);
EXPECT_EQ(dofs.fullDof(0U, fesa::DofComponent::urz), 5U);
EXPECT_EQ(dofs.fullDof(1U, fesa::DofComponent::ux), 6U);
EXPECT_EQ(dofs.fullDof(2U, fesa::DofComponent::urz), 17U);
EXPECT_EQ(
dofs.freeDofs(),
(std::vector<std::size_t>{
3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 12U, 13U, 15U, 16U, 17U}));
EXPECT_EQ(
dofs.constrainedDofs(),
(std::vector<std::size_t>{0U, 1U, 2U, 11U, 14U}));
EXPECT_EQ(dofs.freeEquation(0U), std::nullopt);
EXPECT_EQ(dofs.freeEquation(3U), std::optional<std::size_t>{0U});
EXPECT_EQ(dofs.freeEquation(10U), std::optional<std::size_t>{7U});
EXPECT_EQ(dofs.freeEquation(17U), std::optional<std::size_t>{12U});
}
TEST(DofManager, ExpandsAndValidatesPrescribedValues) {
const auto fixture = makeDofFixture();
const auto& values = fixture.dofs.prescribedValues();
ASSERT_EQ(values.size(), 5U);
EXPECT_DOUBLE_EQ(values[0], 0.0);
EXPECT_DOUBLE_EQ(values[1], 0.0);
EXPECT_DOUBLE_EQ(values[2], 0.25);
EXPECT_DOUBLE_EQ(values[3], -0.5);
EXPECT_DOUBLE_EQ(values[4], 0.25);
auto conflictingDefinition = makeDefinition();
conflictingDefinition.steps[0].boundaries.push_back(
{"root", 1, 1, 1.0, {conflictingDefinition.sourcePath, 77U}});
auto domain = fesa::Domain::create(std::move(conflictingDefinition));
ASSERT_TRUE(domain.hasValue());
auto model = fesa::AnalysisModel::create(domain.value());
ASSERT_TRUE(model.hasValue());
auto conflict = fesa::DofManager::create(model.value());
ASSERT_FALSE(conflict.hasValue());
EXPECT_EQ(conflict.status().failureCategory(), fesa::FailureCategory::input);
ASSERT_EQ(conflict.status().diagnostics().size(), 1U);
const auto& diagnostic = conflict.status().diagnostics()[0];
EXPECT_EQ(diagnostic.code, "conflicting-boundary-condition");
EXPECT_EQ(diagnostic.keyword, "BOUNDARY");
EXPECT_EQ(diagnostic.entityIdentity, "root");
EXPECT_EQ(diagnostic.location.file, std::filesystem::path{"models/dof-manager.inp"});
EXPECT_EQ(diagnostic.location.line, 77U);
}
TEST(DofManager, BuildsTwelveDofScatterAndSortedUniquePattern) {
const auto fixture = makeDofFixture();
const auto& dofs = fixture.dofs;
EXPECT_EQ(
dofs.elementScatter(0U),
(std::array<std::size_t, 12>{
0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U}));
EXPECT_EQ(
dofs.elementScatter(1U),
(std::array<std::size_t, 12>{
6U, 7U, 8U, 9U, 10U, 11U,
12U, 13U, 14U, 15U, 16U, 17U}));
const auto& pattern = dofs.sparsePattern();
EXPECT_EQ(
pattern.rowOffsets,
(std::vector<std::size_t>{
0U, 12U, 24U, 36U, 48U, 60U, 72U,
90U, 108U, 126U, 144U, 162U, 180U,
192U, 204U, 216U, 228U, 240U, 252U}));
const std::vector<std::size_t> firstBlock{
0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U};
const std::vector<std::size_t> sharedBlock{
0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U,
9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U};
const std::vector<std::size_t> lastBlock{
6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U};
EXPECT_EQ(rowColumns(pattern, 0U), firstBlock);
EXPECT_EQ(rowColumns(pattern, 7U), sharedBlock);
EXPECT_EQ(rowColumns(pattern, 17U), lastBlock);
for (std::size_t row = 0U; row < dofs.fullDofCount(); ++row) {
const auto columns = rowColumns(pattern, row);
EXPECT_TRUE(std::is_sorted(columns.begin(), columns.end()));
EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end());
}
}
TEST(DofManager, ReconstructsFullReducedRoundTrip) {
const auto fixture = makeDofFixture();
const auto& dofs = fixture.dofs;
fesa::Vector full{dofs.fullDofCount()};
for (std::size_t index = 0U; index < full.size(); ++index) {
full[index] = static_cast<double>(index) + 0.5;
}
for (std::size_t index = 0U; index < dofs.constrainedDofCount(); ++index) {
full[dofs.constrainedDofs()[index]] = dofs.prescribedValues()[index];
}
fesa::Vector reduced{dofs.freeDofCount()};
for (std::size_t equation = 0U; equation < reduced.size(); ++equation) {
reduced[equation] = full[dofs.freeDofs()[equation]];
}
fesa::Vector reconstructed{dofs.fullDofCount()};
for (std::size_t equation = 0U; equation < reduced.size(); ++equation) {
reconstructed[dofs.freeDofs()[equation]] = reduced[equation];
}
for (std::size_t index = 0U; index < dofs.constrainedDofCount(); ++index) {
reconstructed[dofs.constrainedDofs()[index]] = dofs.prescribedValues()[index];
}
ASSERT_EQ(reconstructed.size(), full.size());
for (std::size_t index = 0U; index < full.size(); ++index) {
EXPECT_DOUBLE_EQ(reconstructed[index], full[index]);
}
}