diff --git a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md index 55991b9..33b7864 100644 --- a/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md +++ b/docs/implementation-plans/linear-static-3d-euler-beam-implementation-report.md @@ -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. diff --git a/include/fesa/fem/dof_manager.hpp b/include/fesa/fem/dof_manager.hpp new file mode 100644 index 0000000..fbcf83a --- /dev/null +++ b/include/fesa/fem/dof_manager.hpp @@ -0,0 +1,65 @@ +#pragma once + +#include "fesa/analysis/analysis_model.hpp" +#include "fesa/math/vector.hpp" + +#include +#include +#include +#include +#include + +namespace fesa { + +enum class DofComponent : std::uint8_t { + ux, + uy, + uz, + urx, + ury, + urz +}; + +struct SparsePattern { + std::vector rowOffsets; + std::vector columnIndices; +}; + +// Owns every equation-space mapping so semantic model objects remain free of +// analysis-specific equation IDs. +class DofManager { +public: + static Result create(const AnalysisModel& model); + + std::size_t fullDofCount() const noexcept; + std::size_t freeDofCount() const noexcept; + std::size_t constrainedDofCount() const noexcept; + std::size_t fullDof(EntityIndex node, DofComponent component) const; + std::optional freeEquation(std::size_t fullDof) const; + const std::array& elementScatter( + EntityIndex element) const; + const std::vector& freeDofs() const noexcept; + const std::vector& constrainedDofs() const noexcept; + const Vector& prescribedValues() const noexcept; + const SparsePattern& sparsePattern() const noexcept; + +private: + DofManager( + std::size_t fullDofCount, + std::vector> freeEquations, + std::vector> elementScatters, + std::vector freeDofs, + std::vector constrainedDofs, + Vector prescribedValues, + SparsePattern sparsePattern); + + std::size_t fullDofCount_; + std::vector> freeEquations_; + std::vector> elementScatters_; + std::vector freeDofs_; + std::vector constrainedDofs_; + Vector prescribedValues_; + SparsePattern sparsePattern_; +}; + +} // namespace fesa diff --git a/src/fesa/CMakeLists.txt b/src/fesa/CMakeLists.txt index b63c4ca..da8d684 100644 --- a/src/fesa/CMakeLists.txt +++ b/src/fesa/CMakeLists.txt @@ -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 diff --git a/src/fesa/fem/dof_manager.cpp b/src/fesa/fem/dof_manager.cpp new file mode 100644 index 0000000..08c886f --- /dev/null +++ b/src/fesa/fem/dof_manager.cpp @@ -0,0 +1,230 @@ +#include "fesa/fem/dof_manager.hpp" + +#include +#include +#include +#include +#include +#include + +namespace fesa { +namespace { + +constexpr std::size_t dofsPerNode = 6U; + +char asciiLower(char value) { + if (value >= 'A' && value <= 'Z') { + return static_cast(value + ('a' - 'A')); + } + return value; +} + +bool equalName(const std::string& left, const std::string& right) { + return left.size() == right.size() && + std::equal( + left.begin(), left.end(), right.begin(), + [](char leftValue, char rightValue) { + return asciiLower(leftValue) == asciiLower(rightValue); + }); +} + +bool tryPositiveInteger(const std::string& text, std::int64_t& value) { + const char* const first = text.data(); + const char* const last = first + text.size(); + const auto parsed = std::from_chars(first, last, value); + return parsed.ec == std::errc{} && parsed.ptr == last && value > 0; +} + +std::vector expandBoundaryTarget( + 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(node)}; + } + } + } + return {}; +} + +SparsePattern buildSparsePattern( + std::size_t fullDofCount, + const std::vector& activeElements, + const std::vector>& elementScatters) { + std::vector> 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::create(const AnalysisModel& model) { + const Domain& domain = model.domain(); + const std::size_t fullCount = domain.nodes().size() * dofsPerNode; + + std::vector> prescribedByFullDof(fullCount); + for (const EntityIndex boundaryIndex : model.activeBoundaryConditions()) { + const auto& boundary = model.step().boundaries.at(boundaryIndex); + const auto target = expandBoundaryTarget(domain, boundary); + for (const EntityIndex node : target) { + for (int component = boundary.firstDof; + component <= boundary.lastDof; + ++component) { + const std::size_t fullDof = + static_cast(node) * dofsPerNode + + static_cast(component - 1); + auto& prescribed = prescribedByFullDof[fullDof]; + if (prescribed && *prescribed != boundary.value) { + return Result::failure(Status::failure( + FailureCategory::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 freeDofs; + std::vector constrainedDofs; + std::vector constrainedValues; + std::vector> freeEquations(fullCount); + freeDofs.reserve(fullCount); + constrainedDofs.reserve(fullCount); + constrainedValues.reserve(fullCount); + // A full-DOF scan fixes free equations, constrained DOFs, and dc in the + // same stable order regardless of boundary declaration overlap. + for (std::size_t fullDof = 0U; fullDof < fullCount; ++fullDof) { + if (prescribedByFullDof[fullDof]) { + constrainedDofs.push_back(fullDof); + constrainedValues.push_back(*prescribedByFullDof[fullDof]); + } else { + freeEquations[fullDof] = freeDofs.size(); + freeDofs.push_back(fullDof); + } + } + + Vector prescribedValues{constrainedValues.size()}; + for (std::size_t index = 0U; index < constrainedValues.size(); ++index) { + prescribedValues[index] = constrainedValues[index]; + } + + std::vector> elementScatters( + domain.elements().size()); + for (const EntityIndex elementIndex : model.activeElements()) { + const auto& element = domain.elements().at(elementIndex); + auto& scatter = elementScatters.at(elementIndex); + for (std::size_t endpoint = 0U; endpoint < element.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::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(component); + if (node >= fullDofCount_ / dofsPerNode || componentIndex >= dofsPerNode) { + throw std::out_of_range{"Node or DOF component is out of range."}; + } + return static_cast(node) * dofsPerNode + componentIndex; +} + +std::optional DofManager::freeEquation( + std::size_t fullDof) const { + return freeEquations_.at(fullDof); +} + +const std::array& DofManager::elementScatter( + EntityIndex element) const { + return elementScatters_.at(element); +} + +const std::vector& DofManager::freeDofs() const noexcept { + return freeDofs_; +} + +const std::vector& 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> freeEquations, + std::vector> elementScatters, + std::vector freeDofs, + std::vector 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 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c95509a..20360da 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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 diff --git a/tests/unit/fem/dof_manager_test.cpp b/tests/unit/fem/dof_manager_test.cpp new file mode 100644 index 0000000..58e274a --- /dev/null +++ b/tests/unit/fem/dof_manager_test.cpp @@ -0,0 +1,191 @@ +#include "fesa/fem/dof_manager.hpp" + +#include + +#include +#include +#include +#include +#include +#include + +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{"Beam-1"}, {0U}, {source, 50U}}, + {"Ends", std::optional{"Beam-1"}, {0U, 2U}, {source, 51U}}}; + definition.steps = {{ + "Step-1", + {{"Root", 1, 2, 0.0, {source, 60U}}, + {"ends", 3, 3, 0.25, {source, 61U}}, + {"20", 6, 6, -0.5, {source, 62U}}, + {"Root", 1, 1, 0.0, {source, 63U}}}, + {}, + 0.1, + 1.0, + 0.01, + 1.0, + {source, 59U}}}; + return definition; +} + +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 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{ + 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 12U, 13U, 15U, 16U, 17U})); + EXPECT_EQ( + dofs.constrainedDofs(), + (std::vector{0U, 1U, 2U, 11U, 14U})); + EXPECT_EQ(dofs.freeEquation(0U), std::nullopt); + EXPECT_EQ(dofs.freeEquation(3U), std::optional{0U}); + EXPECT_EQ(dofs.freeEquation(10U), std::optional{7U}); + EXPECT_EQ(dofs.freeEquation(17U), std::optional{12U}); +} + +TEST(DofManager, ExpandsAndValidatesPrescribedValues) { + const auto fixture = makeDofFixture(); + const auto& values = fixture.dofs.prescribedValues(); + ASSERT_EQ(values.size(), 5U); + EXPECT_DOUBLE_EQ(values[0], 0.0); + EXPECT_DOUBLE_EQ(values[1], 0.0); + EXPECT_DOUBLE_EQ(values[2], 0.25); + EXPECT_DOUBLE_EQ(values[3], -0.5); + EXPECT_DOUBLE_EQ(values[4], 0.25); + + 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{ + 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U})); + EXPECT_EQ( + dofs.elementScatter(1U), + (std::array{ + 6U, 7U, 8U, 9U, 10U, 11U, + 12U, 13U, 14U, 15U, 16U, 17U})); + + const auto& pattern = dofs.sparsePattern(); + EXPECT_EQ( + pattern.rowOffsets, + (std::vector{ + 0U, 12U, 24U, 36U, 48U, 60U, 72U, + 90U, 108U, 126U, 144U, 162U, 180U, + 192U, 204U, 216U, 228U, 240U, 252U})); + const std::vector firstBlock{ + 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U}; + const std::vector sharedBlock{ + 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, + 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; + const std::vector lastBlock{ + 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U, 14U, 15U, 16U, 17U}; + EXPECT_EQ(rowColumns(pattern, 0U), firstBlock); + EXPECT_EQ(rowColumns(pattern, 7U), sharedBlock); + EXPECT_EQ(rowColumns(pattern, 17U), lastBlock); + for (std::size_t row = 0U; row < dofs.fullDofCount(); ++row) { + const auto columns = rowColumns(pattern, row); + EXPECT_TRUE(std::is_sorted(columns.begin(), columns.end())); + EXPECT_EQ(std::adjacent_find(columns.begin(), columns.end()), columns.end()); + } +} + +TEST(DofManager, ReconstructsFullReducedRoundTrip) { + const auto fixture = makeDofFixture(); + const auto& dofs = fixture.dofs; + fesa::Vector full{dofs.fullDofCount()}; + for (std::size_t index = 0U; index < full.size(); ++index) { + full[index] = static_cast(index) + 0.5; + } + for (std::size_t index = 0U; index < dofs.constrainedDofCount(); ++index) { + full[dofs.constrainedDofs()[index]] = dofs.prescribedValues()[index]; + } + + 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]); + } +}