feat(linear-static-3d-euler-beam): step 13 - analysis-model
This commit is contained in:
@@ -359,3 +359,49 @@
|
|||||||
- deferred_scope: the separate known-but-misplaced keyword diagnostic Minor was
|
- deferred_scope: the separate known-but-misplaced keyword diagnostic Minor was
|
||||||
deliberately not changed in this fix round. No phase-index change and no
|
deliberately not changed in this fix round. No phase-index change and no
|
||||||
commit were made.
|
commit were made.
|
||||||
|
|
||||||
|
## Step 13 — analysis-model
|
||||||
|
|
||||||
|
- task_id: `TASK-13`
|
||||||
|
- status: `completed`
|
||||||
|
- changed_files: `include/fesa/analysis/analysis_model.hpp`,
|
||||||
|
`src/fesa/analysis/analysis_model.cpp`,
|
||||||
|
`tests/unit/analysis/analysis_model_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-13-report.md`
|
||||||
|
- requirement_ids: `FESA-REQ-LS3DEB-001`, `FESA-REQ-LS3DEB-021`,
|
||||||
|
`FESA-REQ-LS3DEB-034`
|
||||||
|
- test_ids: `T13-MODEL-001`, `T13-MODEL-002`, `T13-MODEL-003`
|
||||||
|
|
||||||
|
| stage | exact command | exit_code | expected_or_observed_result | evidence_tail |
|
||||||
|
| --- | --- | ---: | --- | --- |
|
||||||
|
| RED-build | `cmake --build .harness/build --config Debug --target fesa_tests` | 1 | Exactly three planned tests were registered before production and the AnalysisModel API was absent | MSVC C1083 reported missing `fesa/analysis/analysis_model.hpp` from `analysis_model_test.cpp` |
|
||||||
|
| RED-test | `ctest --test-dir .harness/build -C Debug -R AnalysisModel --output-on-failure` | 0 | No focused test was runnable because the test executable could not 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 non-owning active view and all three tests compile and link | `analysis_model.cpp`, `analysis_model_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 AnalysisModel --output-on-failure` | 0 | Active classification, address identity/immutability, and cardinality diagnostics pass | 3/3 exact `AnalysisModel` 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 AnalysisModel --output-on-failure` | 0 | Focused Step 13 suite remains green | 3/3 exact `AnalysisModel` tests passed |
|
||||||
|
| VERIFY-discovery | `ctest --test-dir .harness/build -C Debug --show-only=json-v1` | 0 | CTest discovers the accumulated suite and all three exact AnalysisModel names | 22 tests discovered with feature/unit labels |
|
||||||
|
| VERIFY-full | `ctest --test-dir .harness/build -C Debug --output-on-failure` | 0 | Full accumulated C++ suite has zero failures | 22/22 tests passed |
|
||||||
|
| VERIFY-dependency-direction | Backend public-header, upward-analysis dependency, DOF/numeric-state, exact-test-count, registration, and trailing-whitespace scans using fail-on-match `rg` wrappers | 0 | AnalysisModel stays backend-free and below later solver layers, with no DOF/state ownership | backend leaks 0; upward dependencies 0; DOF/numeric-state leaks 0; tests 3; CMake registrations 1/1; new-file trailing whitespace 0 |
|
||||||
|
| VERIFY-diff | `git diff --check` | 0 | Tracked patch has no whitespace errors | Exit 0; only informational Git LF-to-CRLF working-copy notices were emitted |
|
||||||
|
| 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: `AnalysisModel` stores one non-owning `const Domain*` plus
|
||||||
|
stable `EntityIndex` vectors only. All Domain elements are active in stable
|
||||||
|
internal order; reachable material and section IDs are deduplicated by an
|
||||||
|
ascending internal-index scan; boundary/load IDs are the sole step's vector
|
||||||
|
positions. `domain()` and `step()` preserve exact address identity, and the
|
||||||
|
tests verify semantic storage addresses and values remain unchanged. Missing
|
||||||
|
and multiple steps return categorized `invalid-model-cardinality` and
|
||||||
|
`unsupported-multiple-step` diagnostics. No equation numbering, constraint
|
||||||
|
propagation, multi-step activation, or numeric analysis state was added.
|
||||||
|
- generated_evidence: `.harness/build/src/fesa/Debug/fesa_solver.lib`,
|
||||||
|
`.harness/build/tests/Debug/fesa_unit_tests.exe`
|
||||||
|
- reference_diff: unchanged; `git diff --exit-code -- reference/` exit 0
|
||||||
|
- 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.
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "fesa/model/domain.hpp"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
// Provides the sole active-step view while the referenced Domain retains all
|
||||||
|
// semantic ownership and must outlive this object.
|
||||||
|
class AnalysisModel {
|
||||||
|
public:
|
||||||
|
static Result<AnalysisModel> create(const Domain& domain);
|
||||||
|
|
||||||
|
const Domain& domain() const noexcept;
|
||||||
|
const StaticStepDefinition& step() const noexcept;
|
||||||
|
const std::vector<EntityIndex>& activeElements() const noexcept;
|
||||||
|
const std::vector<EntityIndex>& activeMaterials() const noexcept;
|
||||||
|
const std::vector<EntityIndex>& activeSections() const noexcept;
|
||||||
|
const std::vector<EntityIndex>& activeBoundaryConditions() const noexcept;
|
||||||
|
const std::vector<EntityIndex>& activeLoads() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit AnalysisModel(const Domain& domain);
|
||||||
|
|
||||||
|
const Domain* domain_;
|
||||||
|
std::vector<EntityIndex> activeElements_;
|
||||||
|
std::vector<EntityIndex> activeMaterials_;
|
||||||
|
std::vector<EntityIndex> activeSections_;
|
||||||
|
std::vector<EntityIndex> activeBoundaryConditions_;
|
||||||
|
std::vector<EntityIndex> activeLoads_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
add_library(
|
add_library(
|
||||||
fesa_solver
|
fesa_solver
|
||||||
STATIC
|
STATIC
|
||||||
|
analysis/analysis_model.cpp
|
||||||
build_info.cpp
|
build_info.cpp
|
||||||
core/diagnostic.cpp
|
core/diagnostic.cpp
|
||||||
core/status.cpp
|
core/status.cpp
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
#include "fesa/analysis/analysis_model.hpp"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
Result<AnalysisModel> AnalysisModel::create(const Domain& domain) {
|
||||||
|
if (domain.steps().empty()) {
|
||||||
|
return Result<AnalysisModel>::failure(Status::failure(
|
||||||
|
FailureCategory::input,
|
||||||
|
{{Severity::error,
|
||||||
|
"invalid-model-cardinality",
|
||||||
|
{domain.sourcePath(), 0U},
|
||||||
|
"STEP",
|
||||||
|
"0",
|
||||||
|
"AnalysisModel requires exactly one static step."}}));
|
||||||
|
}
|
||||||
|
if (domain.steps().size() > 1U) {
|
||||||
|
const auto& secondStep = domain.steps()[1];
|
||||||
|
return Result<AnalysisModel>::failure(Status::failure(
|
||||||
|
FailureCategory::input,
|
||||||
|
{{Severity::error,
|
||||||
|
"unsupported-multiple-step",
|
||||||
|
secondStep.location,
|
||||||
|
"STEP",
|
||||||
|
secondStep.name,
|
||||||
|
"AnalysisModel does not support multiple steps."}}));
|
||||||
|
}
|
||||||
|
return Result<AnalysisModel>::success(AnalysisModel{domain});
|
||||||
|
}
|
||||||
|
|
||||||
|
const Domain& AnalysisModel::domain() const noexcept {
|
||||||
|
return *domain_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StaticStepDefinition& AnalysisModel::step() const noexcept {
|
||||||
|
return domain_->steps().front();
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<EntityIndex>& AnalysisModel::activeElements() const noexcept {
|
||||||
|
return activeElements_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<EntityIndex>& AnalysisModel::activeMaterials() const noexcept {
|
||||||
|
return activeMaterials_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<EntityIndex>& AnalysisModel::activeSections() const noexcept {
|
||||||
|
return activeSections_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<EntityIndex>&
|
||||||
|
AnalysisModel::activeBoundaryConditions() const noexcept {
|
||||||
|
return activeBoundaryConditions_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<EntityIndex>& AnalysisModel::activeLoads() const noexcept {
|
||||||
|
return activeLoads_;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
|
||||||
|
std::vector<bool> reachableMaterials(domain.materials().size(), false);
|
||||||
|
std::vector<bool> reachableSections(domain.sections().size(), false);
|
||||||
|
|
||||||
|
for (std::size_t index = 0U; index < domain.elements().size(); ++index) {
|
||||||
|
const auto& element = domain.elements()[index];
|
||||||
|
activeElements_.push_back(static_cast<EntityIndex>(index));
|
||||||
|
reachableMaterials[element.materialIndex] = true;
|
||||||
|
reachableSections[element.sectionIndex] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ascending vector positions are the stable internal order, independent
|
||||||
|
// of first reachability or duplicate element assignments.
|
||||||
|
for (std::size_t index = 0U; index < reachableMaterials.size(); ++index) {
|
||||||
|
if (reachableMaterials[index]) {
|
||||||
|
activeMaterials_.push_back(static_cast<EntityIndex>(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < reachableSections.size(); ++index) {
|
||||||
|
if (reachableSections[index]) {
|
||||||
|
activeSections_.push_back(static_cast<EntityIndex>(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t index = 0U;
|
||||||
|
index < step().boundaries.size();
|
||||||
|
++index) {
|
||||||
|
activeBoundaryConditions_.push_back(static_cast<EntityIndex>(index));
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < step().loads.size(); ++index) {
|
||||||
|
activeLoads_.push_back(static_cast<EntityIndex>(index));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -3,6 +3,7 @@ include(GoogleTest)
|
|||||||
add_executable(
|
add_executable(
|
||||||
fesa_unit_tests
|
fesa_unit_tests
|
||||||
unit/build_info_test.cpp
|
unit/build_info_test.cpp
|
||||||
|
unit/analysis/analysis_model_test.cpp
|
||||||
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
|
||||||
|
|||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include "fesa/analysis/analysis_model.hpp"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::ModelDefinition makeDefinition() {
|
||||||
|
const std::filesystem::path source{"models/analysis-model.inp"};
|
||||||
|
fesa::ModelDefinition definition{};
|
||||||
|
definition.sourcePath = source;
|
||||||
|
definition.sourceContentIdentity = "fnv1a64:0123456789abcdef";
|
||||||
|
definition.nodes = {
|
||||||
|
{{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}},
|
||||||
|
{{"Beam-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 11U}},
|
||||||
|
{{"Beam-1", 3, "3"}, {2.0, 0.0, 0.0}, {source, 12U}},
|
||||||
|
{{"Beam-1", 4, "4"}, {3.0, 0.0, 0.0}, {source, 13U}},
|
||||||
|
{{"Beam-1", 5, "5"}, {4.0, 0.0, 0.0}, {source, 14U}}};
|
||||||
|
definition.materials = {
|
||||||
|
{"Material-0", 100.0, 0.20, {source, 20U}},
|
||||||
|
{"Material-1", 200.0, 0.25, {source, 21U}},
|
||||||
|
{"Material-2", 300.0, 0.30, {source, 22U}},
|
||||||
|
{"Unused-Material", 400.0, 0.35, {source, 23U}}};
|
||||||
|
definition.sections = {
|
||||||
|
{"Section-0", 1.0, 1.0, 0.0, 1.0, 1.0,
|
||||||
|
{0.0, 1.0, 0.0}, {}, {source, 30U}},
|
||||||
|
{"Section-1", 2.0, 2.0, 0.0, 2.0, 2.0,
|
||||||
|
{0.0, 1.0, 0.0}, {}, {source, 31U}},
|
||||||
|
{"Section-2", 3.0, 3.0, 0.0, 3.0, 3.0,
|
||||||
|
{0.0, 1.0, 0.0}, {}, {source, 32U}},
|
||||||
|
{"Unused-Section", 4.0, 4.0, 0.0, 4.0, 4.0,
|
||||||
|
{0.0, 1.0, 0.0}, {}, {source, 33U}}};
|
||||||
|
definition.elements = {
|
||||||
|
{{"Beam-1", 1, "1"}, {0U, 1U}, 2U, 2U, {source, 40U}},
|
||||||
|
{{"Beam-1", 2, "2"}, {1U, 2U}, 0U, 1U, {source, 41U}},
|
||||||
|
{{"Beam-1", 3, "3"}, {2U, 3U}, 2U, 2U, {source, 42U}},
|
||||||
|
{{"Beam-1", 4, "4"}, {3U, 4U}, 1U, 0U, {source, 43U}}};
|
||||||
|
definition.steps = {{
|
||||||
|
"Step-1",
|
||||||
|
{{"Root", 1, 3, 0.0, {source, 51U}},
|
||||||
|
{"Root", 4, 6, 0.0, {source, 52U}}},
|
||||||
|
{{"Tip", 1, 10.0, {source, 53U}},
|
||||||
|
{"Tip", 2, -20.0, {source, 54U}},
|
||||||
|
{"Tip", 6, 30.0, {source, 55U}}},
|
||||||
|
0.1,
|
||||||
|
1.0,
|
||||||
|
0.01,
|
||||||
|
1.0,
|
||||||
|
{source, 50U}}};
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(AnalysisModel, ClassifiesActiveEntitiesInStableOrder) {
|
||||||
|
auto domainResult = fesa::Domain::create(makeDefinition());
|
||||||
|
ASSERT_TRUE(domainResult.hasValue());
|
||||||
|
|
||||||
|
auto modelResult = fesa::AnalysisModel::create(domainResult.value());
|
||||||
|
ASSERT_TRUE(modelResult.hasValue());
|
||||||
|
const auto& model = modelResult.value();
|
||||||
|
|
||||||
|
EXPECT_EQ(
|
||||||
|
model.activeElements(),
|
||||||
|
(std::vector<fesa::EntityIndex>{0U, 1U, 2U, 3U}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
model.activeMaterials(),
|
||||||
|
(std::vector<fesa::EntityIndex>{0U, 1U, 2U}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
model.activeSections(),
|
||||||
|
(std::vector<fesa::EntityIndex>{0U, 1U, 2U}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
model.activeBoundaryConditions(),
|
||||||
|
(std::vector<fesa::EntityIndex>{0U, 1U}));
|
||||||
|
EXPECT_EQ(
|
||||||
|
model.activeLoads(),
|
||||||
|
(std::vector<fesa::EntityIndex>{0U, 1U, 2U}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AnalysisModel, ReferencesWithoutCopyingOrMutatingDomain) {
|
||||||
|
auto domainResult = fesa::Domain::create(makeDefinition());
|
||||||
|
ASSERT_TRUE(domainResult.hasValue());
|
||||||
|
const fesa::Domain& domain = domainResult.value();
|
||||||
|
const auto* const elementAddress = domain.elements().data();
|
||||||
|
const auto* const materialAddress = domain.materials().data();
|
||||||
|
const auto* const sectionAddress = domain.sections().data();
|
||||||
|
const std::string stepName = domain.steps()[0].name;
|
||||||
|
const double firstLoadMagnitude = domain.steps()[0].loads[0].magnitude;
|
||||||
|
|
||||||
|
auto modelResult = fesa::AnalysisModel::create(domain);
|
||||||
|
ASSERT_TRUE(modelResult.hasValue());
|
||||||
|
const auto& model = modelResult.value();
|
||||||
|
|
||||||
|
EXPECT_EQ(&model.domain(), &domain);
|
||||||
|
EXPECT_EQ(&model.step(), &domain.steps()[0]);
|
||||||
|
EXPECT_EQ(model.domain().elements().data(), elementAddress);
|
||||||
|
EXPECT_EQ(model.domain().materials().data(), materialAddress);
|
||||||
|
EXPECT_EQ(model.domain().sections().data(), sectionAddress);
|
||||||
|
EXPECT_EQ(
|
||||||
|
&model.domain().elements()[model.activeElements()[1]],
|
||||||
|
&domain.elements()[1]);
|
||||||
|
EXPECT_EQ(
|
||||||
|
&model.domain().materials()[model.activeMaterials()[2]],
|
||||||
|
&domain.materials()[2]);
|
||||||
|
EXPECT_EQ(
|
||||||
|
&model.domain().sections()[model.activeSections()[1]],
|
||||||
|
&domain.sections()[1]);
|
||||||
|
EXPECT_EQ(domain.steps()[0].name, stepName);
|
||||||
|
EXPECT_DOUBLE_EQ(domain.steps()[0].loads[0].magnitude, firstLoadMagnitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AnalysisModel, RejectsMissingOrMultipleStep) {
|
||||||
|
auto missingDefinition = makeDefinition();
|
||||||
|
missingDefinition.steps.clear();
|
||||||
|
auto missingDomain = fesa::Domain::create(std::move(missingDefinition));
|
||||||
|
ASSERT_TRUE(missingDomain.hasValue());
|
||||||
|
|
||||||
|
auto missing = fesa::AnalysisModel::create(missingDomain.value());
|
||||||
|
ASSERT_FALSE(missing.hasValue());
|
||||||
|
EXPECT_EQ(
|
||||||
|
missing.status().failureCategory(),
|
||||||
|
fesa::FailureCategory::input);
|
||||||
|
ASSERT_EQ(missing.status().diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
missing.status().diagnostics()[0].code,
|
||||||
|
"invalid-model-cardinality");
|
||||||
|
EXPECT_EQ(missing.status().diagnostics()[0].keyword, "STEP");
|
||||||
|
EXPECT_EQ(missing.status().diagnostics()[0].entityIdentity, "0");
|
||||||
|
|
||||||
|
auto multipleDefinition = makeDefinition();
|
||||||
|
auto secondStep = multipleDefinition.steps.front();
|
||||||
|
secondStep.name = "Step-2";
|
||||||
|
secondStep.location.line = 60U;
|
||||||
|
multipleDefinition.steps.push_back(std::move(secondStep));
|
||||||
|
auto multipleDomain = fesa::Domain::create(std::move(multipleDefinition));
|
||||||
|
ASSERT_TRUE(multipleDomain.hasValue());
|
||||||
|
|
||||||
|
auto multiple = fesa::AnalysisModel::create(multipleDomain.value());
|
||||||
|
ASSERT_FALSE(multiple.hasValue());
|
||||||
|
EXPECT_EQ(
|
||||||
|
multiple.status().failureCategory(),
|
||||||
|
fesa::FailureCategory::input);
|
||||||
|
ASSERT_EQ(multiple.status().diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
multiple.status().diagnostics()[0].code,
|
||||||
|
"unsupported-multiple-step");
|
||||||
|
EXPECT_EQ(multiple.status().diagnostics()[0].keyword, "STEP");
|
||||||
|
EXPECT_EQ(multiple.status().diagnostics()[0].entityIdentity, "Step-2");
|
||||||
|
EXPECT_EQ(multiple.status().diagnostics()[0].location.line, 60U);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user