feat(linear-static-3d-euler-beam): step 14 - dof-manager
This commit is contained in:
@@ -405,3 +405,57 @@
|
|||||||
- handoff: the exact AnalysisModel ledger API, sole-step reference, and stable
|
- handoff: the exact AnalysisModel ledger API, sole-step reference, and stable
|
||||||
active element/material/section/boundary/load IDs are available to Step 14
|
active element/material/section/boundary/load IDs are available to Step 14
|
||||||
`DofManager` without copying or mutating Domain entities.
|
`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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -5,6 +5,7 @@ add_library(
|
|||||||
build_info.cpp
|
build_info.cpp
|
||||||
core/diagnostic.cpp
|
core/diagnostic.cpp
|
||||||
core/status.cpp
|
core/status.cpp
|
||||||
|
fem/dof_manager.cpp
|
||||||
io/abaqus/domain_mapper.cpp
|
io/abaqus/domain_mapper.cpp
|
||||||
io/abaqus/input_reader.cpp
|
io/abaqus/input_reader.cpp
|
||||||
math/matrix.cpp
|
math/matrix.cpp
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -7,6 +7,7 @@ add_executable(
|
|||||||
unit/core/diagnostic_test.cpp
|
unit/core/diagnostic_test.cpp
|
||||||
unit/core/source_identity_test.cpp
|
unit/core/source_identity_test.cpp
|
||||||
unit/core/status_test.cpp
|
unit/core/status_test.cpp
|
||||||
|
unit/fem/dof_manager_test.cpp
|
||||||
unit/math/matrix_test.cpp
|
unit/math/matrix_test.cpp
|
||||||
unit/math/vector_test.cpp
|
unit/math/vector_test.cpp
|
||||||
unit/io/abaqus/domain_mapper_test.cpp
|
unit/io/abaqus/domain_mapper_test.cpp
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user