From 87529c811a25c0a59cc80ea5ca0116371eee70a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B2=BD=EC=A2=85?= Date: Tue, 9 Jun 2026 15:12:41 +0900 Subject: [PATCH] feat: add analysis state and base --- CMakeLists.txt | 11 + docs/PROGRESS.md | 8 + ...analysis-state-analysis-base-build-test.md | 65 ++++++ ...state-analysis-base-implementation-plan.md | 188 ++++++++++++++++ include/fesa/analysis/Analysis.hpp | 22 ++ include/fesa/core/AnalysisState.hpp | 52 +++++ .../analysis-state-analysis-base/index.json | 42 ++++ phases/analysis-state-analysis-base/step0.md | 62 ++++++ phases/analysis-state-analysis-base/step1.md | 69 ++++++ phases/analysis-state-analysis-base/step2.md | 65 ++++++ phases/analysis-state-analysis-base/step3.md | 69 ++++++ phases/analysis-state-analysis-base/step4.md | 46 ++++ phases/analysis-state-analysis-base/step5.md | 48 +++++ phases/index.json | 4 + src/core/AnalysisState.cpp | 115 ++++++++++ tests/analysis/analysis_base_test.cpp | 78 +++++++ tests/analysis/analysis_test_main.cpp | 12 ++ tests/core/analysis_state_test.cpp | 203 ++++++++++++++++++ 18 files changed, 1159 insertions(+) create mode 100644 docs/build-test-reports/analysis-state-analysis-base-build-test.md create mode 100644 docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md create mode 100644 include/fesa/analysis/Analysis.hpp create mode 100644 include/fesa/core/AnalysisState.hpp create mode 100644 phases/analysis-state-analysis-base/index.json create mode 100644 phases/analysis-state-analysis-base/step0.md create mode 100644 phases/analysis-state-analysis-base/step1.md create mode 100644 phases/analysis-state-analysis-base/step2.md create mode 100644 phases/analysis-state-analysis-base/step3.md create mode 100644 phases/analysis-state-analysis-base/step4.md create mode 100644 phases/analysis-state-analysis-base/step5.md create mode 100644 src/core/AnalysisState.cpp create mode 100644 tests/analysis/analysis_base_test.cpp create mode 100644 tests/analysis/analysis_test_main.cpp create mode 100644 tests/core/analysis_state_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 75f913b..b711ea9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,6 +8,7 @@ set(CMAKE_CXX_EXTENSIONS OFF) add_library(fesa_core STATIC src/boundary/SinglePointConstraint.cpp + src/core/AnalysisState.cpp src/core/Domain.cpp src/element/Mitc4Element.cpp src/io/Hdf5ResultWriter.cpp @@ -61,3 +62,13 @@ target_link_libraries(fesa_io_tests PRIVATE fesa_core) add_test(NAME io.hdf5-result-writer COMMAND fesa_io_tests) set_tests_properties(io.hdf5-result-writer PROPERTIES LABELS "io;hdf5") + +add_executable(fesa_analysis_tests + tests/analysis/analysis_base_test.cpp + tests/analysis/analysis_test_main.cpp + tests/core/analysis_state_test.cpp +) +target_link_libraries(fesa_analysis_tests PRIVATE fesa_core) + +add_test(NAME analysis.base COMMAND fesa_analysis_tests) +set_tests_properties(analysis.base PROPERTIES LABELS "analysis;core") diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index b79daac..8dd899c 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -41,6 +41,10 @@ - Created `docs/implementation-plans/property-model-foundation-implementation-plan.md` and `phases/property-model-foundation/`. - Added runtime `Property` base class and `PropertyKind`, made `ShellProperty` derive from `Property`, and migrated `Domain` property ownership to `std::unique_ptr`. - Added a minimal `Hdf5ResultWriter` skeleton with path validation only; it does not link HDF5 or write files yet. +- Created `docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` and `phases/analysis-state-analysis-base/`. +- Added `AnalysisState` mutable vector storage for displacement, external force, internal force, residual, and reaction, plus resize, guarded setters, force clearing, and time/increment/iteration counters. +- Added `Analysis` base interface with const `Domain` input, mutable `AnalysisState`, `name()`, virtual deletion, and `run` delegation to derived analysis implementations. +- Added `fesa_analysis_tests` CTest target and analysis/core unit tests. ## In Progress - Ready for the next upstream MITC4 stage: new solver feature requirements analysis for `mitc4-linear-static-shell`. @@ -54,6 +58,10 @@ 6. Create `docs/implementation-plans/mitc4-linear-static-shell-implementation-plan.md`. ## Last Validation +- 2026-06-09: After analysis state and analysis base implementation, `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis|domain|model-object|io"` passed. 4 CTest executables ran successfully. +- 2026-06-09: After analysis state and analysis base implementation, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully. +- 2026-06-09: After analysis state and analysis base implementation, `python scripts/validate_workspace.py` configured CMake with Visual Studio 17 2022 x64, built Debug targets, ran CTest, and passed. +- 2026-06-09: After analysis state and analysis base implementation, `git diff --check` passed with only Git line-ending normalization warnings. - 2026-06-09: After analysis model object implementation, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully. - 2026-06-09: After analysis model object implementation, `python scripts/validate_workspace.py` configured CMake with Visual Studio 17 2022 x64, built Debug targets, ran CTest, and passed. - 2026-06-09: After analysis model object implementation, `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"` passed. 2 CTest executables ran successfully. diff --git a/docs/build-test-reports/analysis-state-analysis-base-build-test.md b/docs/build-test-reports/analysis-state-analysis-base-build-test.md new file mode 100644 index 0000000..5282d36 --- /dev/null +++ b/docs/build-test-reports/analysis-state-analysis-base-build-test.md @@ -0,0 +1,65 @@ +# Analysis State And Analysis Base Build/Test Report + +## Metadata +- feature_id: analysis-state-analysis-base +- source_implementation_report: N/A +- source_implementation_plan: docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md +- status: pass-for-next-implementation-stage +- owner_agent: build-test-executor-agent +- date: 2026-06-09 + +## Execution Environment +- os: Windows +- generator: Visual Studio 17 2022 +- platform: x64 +- config: Debug +- build_dir: build/msvc-debug +- active_override_env_vars: none observed +- command_discovery_path: default CMake/MSVC x64 Debug through scripts/validate_workspace.py + +## Command Log Summary + +| order | command | exit_code | duration | stdout_stderr_tail | +| --- | --- | --- | --- | --- | +| 1 | `python scripts/validate_workspace.py` after RED tests | 1 | short | Expected compile failure: missing `fesa/analysis/Analysis.hpp` and `fesa/core/AnalysisState.hpp`. | +| 2 | `C:\Program Files\CMake\bin\ctest.exe --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis` | 0 | 0.39 sec | `analysis.base` passed. | +| 3 | `C:\Program Files\CMake\bin\ctest.exe --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis\|domain\|model-object\|io"` | 0 | 1.31 sec | 4 tests passed. | +| 4 | `python -m unittest discover -s scripts -p "test_*.py"` | 0 | 0.230 sec | 89 tests ran successfully. | +| 5 | `python scripts/validate_workspace.py` | 0 | short | Configure, build, and CTest passed; 4 CTest tests passed. | +| 6 | `git diff --check` | 0 | short | Passed with Git LF-to-CRLF normalization warnings only. | + +## Validation Results + +| validation_stage | result | evidence | +| --- | --- | --- | +| RED check | pass | Initial build failed because planned headers/classes were missing. | +| targeted analysis CTest | pass | `analysis.base` passed. | +| regression CTest | pass | `domain.bootstrap`, `model-object.base`, `io.hdf5-result-writer`, and `analysis.base` passed. | +| harness self-test | pass | 89 Python tests passed. | +| workspace validation | pass | Configure, Debug build, and all CTest tests passed. | +| whitespace check | pass | `git diff --check` returned exit code 0. | + +## Failure Classification + +- classification: N/A after implementation +- primary_failure: N/A +- first_failed_command: N/A +- evidence_tail: N/A + +## Handoff Recommendation + +| target_agent | reason | required_input | +| --- | --- | --- | +| AnalysisModel Implementation Agent | Analysis base and mutable state storage now exist. | `include/fesa/analysis/Analysis.hpp`, `include/fesa/core/AnalysisState.hpp`, `tests/analysis/analysis_base_test.cpp`, `tests/core/analysis_state_test.cpp` | +| DofManager Implementation Agent | State vectors are size-based but do not define equation numbering. | `AnalysisState` contract and architecture rules | +| LinearStaticAnalysis Implementation Agent | Base strategy entry point exists, but no solver algorithm is implemented. | future `AnalysisModel` and `DofManager` contracts | + +## No-Change Assertion +- source_files_modified: true +- test_files_modified: true +- cmake_files_modified: true +- reference_artifacts_modified: false +- notes: No requirements, formulations, I/O contracts, reference artifacts, or tolerance policies were modified. + +## Open Issues +- `AnalysisModel`, `DofManager`, and `LinearStaticAnalysis` remain separate downstream phases. diff --git a/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md b/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md new file mode 100644 index 0000000..680ec4b --- /dev/null +++ b/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md @@ -0,0 +1,188 @@ +# Analysis State And Analysis Base Implementation Plan + +## Metadata +- feature_id: analysis-state-analysis-base +- source_requirement: AGENTS.md; docs/PRD.md +- source_research: N/A for this architecture foundation slice +- source_formulation: N/A for this architecture foundation slice +- source_numerical_review: N/A for this architecture foundation slice +- source_io_definition: docs/ARCHITECTURE.md state ownership and analysis strategy rules +- source_reference_models: N/A for this architecture foundation slice +- status: ready-for-implementation +- owner_agent: implementation-planning-agent +- date: 2026-06-09 + +## Readiness Check + +| input | required_status | observed_status | decision | +| --- | --- | --- | --- | +| architecture | Domain, Analysis, AnalysisState boundaries documented | documented in docs/ARCHITECTURE.md and ADR-010 | proceed | +| domain foundation | Domain runtime storage available | implemented in prior phases | proceed | +| formulation | not required for state/interface foundation | N/A | proceed | +| numerical_review | not required for state/interface foundation | N/A | proceed | +| io_definition | not required; no result output in this phase | N/A | proceed | +| reference_models | not required because this phase produces no solver result | N/A | proceed | + +## Implementation Scope + +- included_behavior: `AnalysisState` mutable vector state for displacement, external force, internal force, residual, and reaction. +- included_behavior: `AnalysisState` time, increment, and iteration counters. +- included_behavior: `Analysis` base interface for future analysis strategies. +- included_behavior: CMake/CTest registration for analysis-layer unit tests. +- excluded_behavior: `AnalysisModel`, `DofManager`, equation numbering, global assembly, boundary-condition elimination, linear solve, MITC4 numerical formulation, HDF5 output, and reference comparison. +- non_goals: numerical correctness claims, release readiness, reference-solver execution, and reference artifact generation. + +## AnalysisState Contract + +`AnalysisState` lives under `fesa::core` and owns mutable analysis quantities only. It does not reference or own `Domain`, model objects, equation maps, sparse matrices, solvers, result writers, or reference artifacts. + +Required interface: + +```cpp +namespace fesa::core { + +class AnalysisState { +public: + AnalysisState(); + explicit AnalysisState(std::size_t dof_count); + + std::size_t dofCount() const noexcept; + void resize(std::size_t dof_count); + + const std::vector& displacement() const noexcept; + const std::vector& externalForce() const noexcept; + const std::vector& internalForce() const noexcept; + const std::vector& residual() const noexcept; + const std::vector& reaction() const noexcept; + + void setDisplacement(std::vector values); + void setExternalForce(std::vector values); + void setInternalForce(std::vector values); + void setResidual(std::vector values); + void setReaction(std::vector values); + void clearForces() noexcept; + + double currentTime() const noexcept; + void setCurrentTime(double value) noexcept; + std::int64_t incrementIndex() const noexcept; + void setIncrementIndex(std::int64_t value) noexcept; + std::int64_t iterationIndex() const noexcept; + void setIterationIndex(std::int64_t value) noexcept; +}; + +} // namespace fesa::core +``` + +This phase intentionally defers velocity, acceleration, temperature, element state, and integration-point state until dynamic, thermal, nonlinear, or element-state phases define concrete contracts. + +## Analysis Base Contract + +`Analysis` lives under `fesa::analysis` and is the base strategy interface for future analysis algorithms. `Domain` is immutable input. `AnalysisState` is mutable output/state. + +Required interface: + +```cpp +namespace fesa::analysis { + +class Analysis { +public: + virtual ~Analysis() = default; + + virtual const char* name() const noexcept = 0; + void run(const fesa::core::Domain& domain, fesa::core::AnalysisState& state); + +protected: + virtual void doRun(const fesa::core::Domain& domain, fesa::core::AnalysisState& state) = 0; +}; + +} // namespace fesa::analysis +``` + +`Analysis::run` is only an entry-point wrapper in this phase. It does not define assembly, solve, boundary-condition, or output hooks until `AnalysisModel` and `DofManager` exist. + +## Work Breakdown + +| task_id | order | purpose | upstream_trace | depends_on | expected_test_first | +| --- | --- | --- | --- | --- | --- | +| ASAB-001 | 1 | record state and base-analysis contract | ADR-010; docs/ARCHITECTURE.md | none | N/A | +| ASAB-002 | 2 | add `AnalysisState` zero-sized and sized state | ADR-010 AnalysisState | ASAB-001 | ASAB-TEST-001 | +| ASAB-003 | 3 | add `AnalysisState` mutation guards and counters | docs/ARCHITECTURE.md State Ownership | ASAB-002 | ASAB-TEST-002 | +| ASAB-004 | 4 | add `Analysis` base interface | docs/ARCHITECTURE.md Analysis strategy | ASAB-003 | ASAB-TEST-003 | +| ASAB-005 | 5 | register analysis CTest path | AGENTS.md C++ validation | ASAB-004 | ASAB-TEST-004 | +| ASAB-006 | 6 | record build/test evidence and handoff | docs/build-test-reports/README.md | ASAB-005 | ASAB-TEST-005 | + +## TDD Test Plan + +| test_id | order | test_type | red_condition | green_condition | linked_task | command | +| --- | --- | --- | --- | --- | --- | --- | +| ASAB-TEST-001 | 1 | unit | `AnalysisState` header/class missing | default and sized state tests pass | ASAB-002 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis` | +| ASAB-TEST-002 | 2 | unit | setter/guard methods missing | mutation guard and counter tests pass | ASAB-003 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis` | +| ASAB-TEST-003 | 3 | unit | `Analysis` header/base missing | derived recording analysis tests pass | ASAB-004 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis` | +| ASAB-TEST-004 | 4 | integration | analysis tests not registered or not built | `ctest -R analysis` runs analysis target | ASAB-005 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis|domain|model-object|io"` | +| ASAB-TEST-005 | 5 | validation | report evidence missing | validation report records passing commands or classified failure | ASAB-006 | `python scripts/validate_workspace.py` | + +## CMake/CTest Plan + +- target_candidates: `fesa_core`, `fesa_analysis_tests` +- add_test_needs: register `analysis.base` +- labels: `analysis`, `core` +- msvc_config: Debug +- expected_feature_command: `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis` +- workspace_validation: `python scripts/validate_workspace.py` + +## Candidate Files and Ownership + +| file_candidate | purpose | owner_boundary | notes | +| --- | --- | --- | --- | +| `include/fesa/core/AnalysisState.hpp` | mutable analysis state public contract | core state | no Domain/model object ownership | +| `src/core/AnalysisState.cpp` | state vector/counter implementation | core state | no solver logic | +| `include/fesa/analysis/Analysis.hpp` | base analysis strategy interface | analysis | no LinearStaticAnalysis implementation | +| `tests/core/analysis_state_test.cpp` | state TDD coverage | tests | write before production changes | +| `tests/analysis/analysis_base_test.cpp` | analysis base TDD coverage | tests | write before production changes | +| `CMakeLists.txt` | source and CTest registration | build | MSVC Debug compatible | + +## Acceptance Traceability Matrix + +| requirement_id | task_id | test_id | reference_model_id | acceptance_criterion | status | +| --- | --- | --- | --- | --- | --- | +| ASAB-REQ-001 mutable state is outside Domain | ASAB-002 | ASAB-TEST-001 | N/A | AnalysisState tests pass and Domain remains unchanged | draft | +| ASAB-REQ-002 state vectors are size-consistent | ASAB-003 | ASAB-TEST-002 | N/A | size mismatch throws and failed setters do not mutate | draft | +| ASAB-REQ-003 Analysis takes const Domain and mutable state | ASAB-004 | ASAB-TEST-003 | N/A | recording analysis updates state through base API | draft | +| ASAB-REQ-004 C++ validation path includes analysis tests | ASAB-005 | ASAB-TEST-004 | N/A | `ctest -R analysis` runs successfully | draft | + +## Validation Commands + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis|domain|model-object|io" +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +git diff --check +``` + +## Risks and Downstream Handoff + +### Implementation Agent + +- Keep `AnalysisState` as storage only. +- Keep `Analysis` as a base interface only. +- Do not introduce `LinearStaticAnalysis` until `AnalysisModel` and `DofManager` contracts exist. + +### Build/Test Executor Agent + +- Use Visual Studio 17 2022, x64, Debug, and `build/msvc-debug`. +- Use `python scripts/validate_workspace.py` as canonical validation. + +### Correction Agent + +- Implementation-owned failures are expected to be compile or unit-test failures in state headers, state sources, analysis header, analysis tests, or CMake registration. +- Upstream-contract failures include requests to add equation numbering, assembly, solver behavior, HDF5 output, or numerical MITC4 behavior in this phase. + +### Reference Verification Agent + +- No reference verification is required for this phase. +- This phase produces no HDF5 result and consumes no reference artifacts. + +## Open Issues + +- `AnalysisModel`, `DofManager`, and `LinearStaticAnalysis` remain separate downstream phases. diff --git a/include/fesa/analysis/Analysis.hpp b/include/fesa/analysis/Analysis.hpp new file mode 100644 index 0000000..1499f84 --- /dev/null +++ b/include/fesa/analysis/Analysis.hpp @@ -0,0 +1,22 @@ +#pragma once + +#include "fesa/core/AnalysisState.hpp" +#include "fesa/core/Domain.hpp" + +namespace fesa::analysis { + +class Analysis { +public: + virtual ~Analysis() = default; + + virtual const char* name() const noexcept = 0; + + void run(const fesa::core::Domain& domain, fesa::core::AnalysisState& state) { + doRun(domain, state); + } + +protected: + virtual void doRun(const fesa::core::Domain& domain, fesa::core::AnalysisState& state) = 0; +}; + +} // namespace fesa::analysis diff --git a/include/fesa/core/AnalysisState.hpp b/include/fesa/core/AnalysisState.hpp new file mode 100644 index 0000000..c29435b --- /dev/null +++ b/include/fesa/core/AnalysisState.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include +#include +#include + +namespace fesa::core { + +class AnalysisState { +public: + AnalysisState(); + explicit AnalysisState(std::size_t dof_count); + + std::size_t dofCount() const noexcept; + void resize(std::size_t dof_count); + + const std::vector& displacement() const noexcept; + const std::vector& externalForce() const noexcept; + const std::vector& internalForce() const noexcept; + const std::vector& residual() const noexcept; + const std::vector& reaction() const noexcept; + + void setDisplacement(std::vector values); + void setExternalForce(std::vector values); + void setInternalForce(std::vector values); + void setResidual(std::vector values); + void setReaction(std::vector values); + void clearForces() noexcept; + + double currentTime() const noexcept; + void setCurrentTime(double value) noexcept; + std::int64_t incrementIndex() const noexcept; + void setIncrementIndex(std::int64_t value) noexcept; + std::int64_t iterationIndex() const noexcept; + void setIterationIndex(std::int64_t value) noexcept; + +private: + void setVector(std::vector& target, std::vector values); + static void zero(std::vector& values) noexcept; + + std::size_t dof_count_; + std::vector displacement_; + std::vector external_force_; + std::vector internal_force_; + std::vector residual_; + std::vector reaction_; + double current_time_; + std::int64_t increment_index_; + std::int64_t iteration_index_; +}; + +} // namespace fesa::core diff --git a/phases/analysis-state-analysis-base/index.json b/phases/analysis-state-analysis-base/index.json new file mode 100644 index 0000000..1e8ac8a --- /dev/null +++ b/phases/analysis-state-analysis-base/index.json @@ -0,0 +1,42 @@ +{ + "project": "FESA Structural Solver", + "phase": "analysis-state-analysis-base", + "steps": [ + { + "step": 0, + "name": "analysis-contract-plan", + "status": "completed", + "summary": "Created the analysis state and analysis base implementation plan with scope, contracts, TDD tasks, and exclusions." + }, + { + "step": 1, + "name": "analysis-state-core", + "status": "completed", + "summary": "Added AnalysisState with zero-sized and sized vector storage for displacement, forces, residual, and reaction." + }, + { + "step": 2, + "name": "analysis-state-guards", + "status": "completed", + "summary": "Added AnalysisState setters, size guards, resize reset, clearForces, and time/increment/iteration counters." + }, + { + "step": 3, + "name": "analysis-base-interface", + "status": "completed", + "summary": "Added the Analysis base interface with const Domain input, mutable AnalysisState, name contract, virtual deletion, and run delegation." + }, + { + "step": 4, + "name": "cmake-analysis-tests", + "status": "completed", + "summary": "Registered fesa_analysis_tests and confirmed analysis, domain, model-object, and io CTest targets pass together." + }, + { + "step": 5, + "name": "validation-report-handoff", + "status": "completed", + "summary": "Recorded build/test evidence, updated project progress, and marked the phase completed." + } + ] +} diff --git a/phases/analysis-state-analysis-base/step0.md b/phases/analysis-state-analysis-base/step0.md new file mode 100644 index 0000000..5d895e5 --- /dev/null +++ b/phases/analysis-state-analysis-base/step0.md @@ -0,0 +1,62 @@ +# Step 0: analysis-contract-plan + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/PRD.md` +- `/docs/ADR.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/docs/implementation-plans/property-model-foundation-implementation-plan.md` +- `/include/fesa/core/Domain.hpp` +- `/include/fesa/core/ModelTypes.hpp` +- `/include/fesa/core/StepDefinition.hpp` +- `/CMakeLists.txt` + +## Task + +Create `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md`. + +The document must define the implementation contract for: + +- `fesa::core::AnalysisState` as mutable analysis state storage. +- `fesa::analysis::Analysis` as the base analysis strategy interface. +- A dedicated CTest path for analysis-layer tests. + +State explicitly that this phase does not implement: + +- `AnalysisModel` +- `DofManager` +- MITC4 stiffness, stress, resultants, or force recovery +- global assembly +- constrained DOF elimination +- MKL/PARDISO solve +- HDF5 result writing beyond existing skeletons +- reference comparison +- Abaqus/Nastran/reference-solver execution + +## Tests To Write First + +This is a documentation planning step. Do not write C++ production code in this step. + +## Acceptance Criteria + +Run: + +```powershell +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-state-analysis-base/index.json` step 0 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not change C++ code in this step. +- Do not change requirements, formulations, I/O contracts, reference artifacts, or tolerance policy. diff --git a/phases/analysis-state-analysis-base/step1.md b/phases/analysis-state-analysis-base/step1.md new file mode 100644 index 0000000..ba139e9 --- /dev/null +++ b/phases/analysis-state-analysis-base/step1.md @@ -0,0 +1,69 @@ +# Step 1: analysis-state-core + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/ADR.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` +- `/include/fesa/core/ModelTypes.hpp` +- `/include/fesa/core/Domain.hpp` +- `/CMakeLists.txt` + +## Task + +Add `fesa::core::AnalysisState`. + +Candidate files: + +- Create `/include/fesa/core/AnalysisState.hpp` +- Create `/src/core/AnalysisState.cpp` +- Create `/tests/core/analysis_state_test.cpp` +- Modify `/CMakeLists.txt` + +Required initial contract: + +- `AnalysisState()` starts with zero DOFs and empty vectors. +- `explicit AnalysisState(std::size_t dof_count)` allocates zero-filled vectors. +- `dofCount()` returns the current vector size. +- `displacement()`, `externalForce()`, `internalForce()`, `residual()`, and `reaction()` return const vector references. +- All state vectors have the same size as `dofCount()`. +- State is mutable analysis data only. Do not store node ids, element ids, equation ids, sparse data, solver handles, writer handles, or references to `Domain`. + +## Tests To Write First + +Write tests in `/tests/core/analysis_state_test.cpp` before creating production code: + +- default state has zero DOFs and all vectors empty. +- sized state creates zero-filled displacement, external force, internal force, residual, and reaction vectors. +- `dofCount()` matches vector sizes. + +Run the targeted test and confirm it fails because `AnalysisState` does not exist: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +``` + +Then implement the minimum code needed to pass. + +## Acceptance Criteria + +Run: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-state-analysis-base/index.json` step 1 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not implement `AnalysisModel`, `DofManager`, assembly, solver, or result output. +- Do not add mutable solver state to `Domain`, `Node`, `Element`, `Material`, `Property`, `Load`, or `BoundaryCondition`. diff --git a/phases/analysis-state-analysis-base/step2.md b/phases/analysis-state-analysis-base/step2.md new file mode 100644 index 0000000..6738128 --- /dev/null +++ b/phases/analysis-state-analysis-base/step2.md @@ -0,0 +1,65 @@ +# Step 2: analysis-state-guards + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` +- `/include/fesa/core/AnalysisState.hpp` +- `/src/core/AnalysisState.cpp` +- `/tests/core/analysis_state_test.cpp` +- `/CMakeLists.txt` + +## Task + +Extend `AnalysisState` with mutation and guard behavior. + +Required contract: + +- `resize(std::size_t dof_count)` resets every vector to the new size and fills with zero. +- `setDisplacement(std::vector)`, `setExternalForce(std::vector)`, `setInternalForce(std::vector)`, `setResidual(std::vector)`, and `setReaction(std::vector)` replace one vector. +- setter inputs must match `dofCount()`. +- size mismatch throws `std::invalid_argument`. +- a failed setter must not mutate the existing vector. +- `clearForces()` zeros external force, internal force, residual, and reaction while leaving displacement unchanged. +- `currentTime()`, `incrementIndex()`, and `iterationIndex()` accessors exist with setters. + +## Tests To Write First + +Add tests in `/tests/core/analysis_state_test.cpp` before production changes: + +- setters replace vectors when sizes match. +- mismatched setter input throws and preserves old values. +- resize resets all vectors to zero. +- clearForces preserves displacement and zeros force-like vectors. +- time, increment, and iteration counters can be updated. + +Run the targeted test and confirm it fails before implementation: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +``` + +Then implement the minimum code needed to pass. + +## Acceptance Criteria + +Run: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-state-analysis-base/index.json` step 2 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not add dynamics-only velocity or acceleration vectors in this phase. +- Do not add thermal temperature fields in this phase. +- Do not add equation numbering or DOF mapping responsibilities. diff --git a/phases/analysis-state-analysis-base/step3.md b/phases/analysis-state-analysis-base/step3.md new file mode 100644 index 0000000..ba2bfdd --- /dev/null +++ b/phases/analysis-state-analysis-base/step3.md @@ -0,0 +1,69 @@ +# Step 3: analysis-base-interface + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/ADR.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` +- `/include/fesa/core/Domain.hpp` +- `/include/fesa/core/AnalysisState.hpp` +- `/CMakeLists.txt` + +## Task + +Add `fesa::analysis::Analysis` as a minimal base interface for future analysis strategies. + +Candidate files: + +- Create `/include/fesa/analysis/Analysis.hpp` +- Create `/tests/analysis/analysis_base_test.cpp` +- Modify `/CMakeLists.txt` + +Required contract: + +- `Analysis` has a virtual destructor. +- `Analysis` exposes `const char* name() const noexcept`. +- `Analysis` exposes `void run(const fesa::core::Domain& domain, fesa::core::AnalysisState& state)`. +- `run` delegates to a protected pure virtual `doRun(const fesa::core::Domain&, fesa::core::AnalysisState&)`. +- `Domain` is passed as const; `AnalysisState` is mutable. +- The base class must not own `Domain`, `AnalysisState`, solver objects, result writers, or reference artifacts. + +## Tests To Write First + +Write tests in `/tests/analysis/analysis_base_test.cpp` before production changes: + +- a derived recording analysis can be used through `Analysis&`. +- `run` forwards the const `Domain` and mutable `AnalysisState` to the derived implementation. +- derived implementation can update `AnalysisState` without mutating `Domain`. +- `Analysis` can be deleted through a base pointer. + +Run the targeted test and confirm it fails before implementation: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +``` + +Then implement the minimum code needed to pass. + +## Acceptance Criteria + +Run: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R analysis +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-state-analysis-base/index.json` step 3 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not implement `LinearStaticAnalysis`. +- Do not add template-method hook lists for assembly, solve, boundary conditions, or output until `AnalysisModel` and `DofManager` exist. diff --git a/phases/analysis-state-analysis-base/step4.md b/phases/analysis-state-analysis-base/step4.md new file mode 100644 index 0000000..86c2141 --- /dev/null +++ b/phases/analysis-state-analysis-base/step4.md @@ -0,0 +1,46 @@ +# Step 4: cmake-analysis-tests + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` +- `/CMakeLists.txt` +- `/tests/core/analysis_state_test.cpp` +- `/tests/analysis/analysis_base_test.cpp` + +## Task + +Make the analysis tests part of the normal MSVC CMake/CTest path. + +Required contract: + +- `fesa_core` includes `src/core/AnalysisState.cpp`. +- `fesa_analysis_tests` builds both analysis-state and analysis-base tests. +- CTest registers `analysis.base` or equivalent with labels `analysis;core`. +- Existing `domain`, `model-object`, and `io` tests remain registered and unchanged except for required shared library source additions. + +## Tests To Write First + +No new behavior tests are required in this step if steps 1 through 3 already wrote them. If CMake registration is missing, the RED condition is that `ctest -R analysis` finds no registered analysis test or build fails because sources are missing. + +## Acceptance Criteria + +Run: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis|domain|model-object|io" +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-state-analysis-base/index.json` step 4 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not modify unrelated tests. +- Do not change dependency discovery or introduce MKL, TBB, or HDF5 linkage for this phase. diff --git a/phases/analysis-state-analysis-base/step5.md b/phases/analysis-state-analysis-base/step5.md new file mode 100644 index 0000000..bdae0cf --- /dev/null +++ b/phases/analysis-state-analysis-base/step5.md @@ -0,0 +1,48 @@ +# Step 5: validation-report-handoff + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/PLAN.md` +- `/docs/PROGRESS.md` +- `/docs/WORKNOTE.md` +- `/docs/AGENT_RULES.md` +- `/docs/implementation-plans/analysis-state-analysis-base-implementation-plan.md` +- `/docs/build-test-reports/README.md` +- `/phases/analysis-state-analysis-base/index.json` + +## Task + +Run final validation, record evidence, and update handoff files. + +Required output: + +- Create `/docs/build-test-reports/analysis-state-analysis-base-build-test.md`. +- Update `/docs/PROGRESS.md` with completed work and validation evidence. +- Update `/phases/analysis-state-analysis-base/index.json` step 5. +- Update `/phases/index.json` phase status to `completed` only if validation passes. + +## Tests To Write First + +No new tests are required in this reporting step. + +## Acceptance Criteria + +Run: + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "analysis|domain|model-object|io" +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +git diff --check +``` + +Record command outcomes in `/docs/build-test-reports/analysis-state-analysis-base-build-test.md`. + +## Do Not + +- Do not claim numerical correctness. +- Do not claim release readiness. +- Do not run Abaqus, Nastran, or any reference solver. diff --git a/phases/index.json b/phases/index.json index d7b9606..9966c05 100644 --- a/phases/index.json +++ b/phases/index.json @@ -15,6 +15,10 @@ { "dir": "property-model-foundation", "status": "completed" + }, + { + "dir": "analysis-state-analysis-base", + "status": "completed" } ] } diff --git a/src/core/AnalysisState.cpp b/src/core/AnalysisState.cpp new file mode 100644 index 0000000..6c359e1 --- /dev/null +++ b/src/core/AnalysisState.cpp @@ -0,0 +1,115 @@ +#include "fesa/core/AnalysisState.hpp" + +#include +#include +#include + +namespace fesa::core { + +AnalysisState::AnalysisState() + : AnalysisState(0) {} + +AnalysisState::AnalysisState(std::size_t dof_count) + : dof_count_(0), + current_time_(0.0), + increment_index_(0), + iteration_index_(0) { + resize(dof_count); +} + +std::size_t AnalysisState::dofCount() const noexcept { + return dof_count_; +} + +void AnalysisState::resize(std::size_t dof_count) { + dof_count_ = dof_count; + displacement_.assign(dof_count_, 0.0); + external_force_.assign(dof_count_, 0.0); + internal_force_.assign(dof_count_, 0.0); + residual_.assign(dof_count_, 0.0); + reaction_.assign(dof_count_, 0.0); +} + +const std::vector& AnalysisState::displacement() const noexcept { + return displacement_; +} + +const std::vector& AnalysisState::externalForce() const noexcept { + return external_force_; +} + +const std::vector& AnalysisState::internalForce() const noexcept { + return internal_force_; +} + +const std::vector& AnalysisState::residual() const noexcept { + return residual_; +} + +const std::vector& AnalysisState::reaction() const noexcept { + return reaction_; +} + +void AnalysisState::setDisplacement(std::vector values) { + setVector(displacement_, std::move(values)); +} + +void AnalysisState::setExternalForce(std::vector values) { + setVector(external_force_, std::move(values)); +} + +void AnalysisState::setInternalForce(std::vector values) { + setVector(internal_force_, std::move(values)); +} + +void AnalysisState::setResidual(std::vector values) { + setVector(residual_, std::move(values)); +} + +void AnalysisState::setReaction(std::vector values) { + setVector(reaction_, std::move(values)); +} + +void AnalysisState::clearForces() noexcept { + zero(external_force_); + zero(internal_force_); + zero(residual_); + zero(reaction_); +} + +double AnalysisState::currentTime() const noexcept { + return current_time_; +} + +void AnalysisState::setCurrentTime(double value) noexcept { + current_time_ = value; +} + +std::int64_t AnalysisState::incrementIndex() const noexcept { + return increment_index_; +} + +void AnalysisState::setIncrementIndex(std::int64_t value) noexcept { + increment_index_ = value; +} + +std::int64_t AnalysisState::iterationIndex() const noexcept { + return iteration_index_; +} + +void AnalysisState::setIterationIndex(std::int64_t value) noexcept { + iteration_index_ = value; +} + +void AnalysisState::setVector(std::vector& target, std::vector values) { + if (values.size() != dof_count_) { + throw std::invalid_argument("analysis state vector size mismatch"); + } + target = std::move(values); +} + +void AnalysisState::zero(std::vector& values) noexcept { + std::fill(values.begin(), values.end(), 0.0); +} + +} // namespace fesa::core diff --git a/tests/analysis/analysis_base_test.cpp b/tests/analysis/analysis_base_test.cpp new file mode 100644 index 0000000..a6e0550 --- /dev/null +++ b/tests/analysis/analysis_base_test.cpp @@ -0,0 +1,78 @@ +#include "fesa/analysis/Analysis.hpp" + +#include "fesa/core/AnalysisState.hpp" +#include "fesa/core/Domain.hpp" + +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +class RecordingAnalysis final : public fesa::analysis::Analysis { +public: + const char* name() const noexcept override { + return "recording"; + } + + bool received_domain = false; + bool ran = false; + +protected: + void doRun(const fesa::core::Domain& domain, fesa::core::AnalysisState& state) override { + received_domain = domain.nodeCount() == 1; + state.setCurrentTime(2.0); + ran = true; + } +}; + +int derived_analysis_runs_through_base_api() { + fesa::core::Domain domain; + domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0}); + fesa::core::AnalysisState state; + RecordingAnalysis analysis; + + fesa::analysis::Analysis& base = analysis; + base.run(domain, state); + + if (const int result = require(analysis.ran); result != 0) { + return result; + } + if (const int result = require(analysis.received_domain); result != 0) { + return result; + } + if (const int result = require(domain.nodeCount() == 1); result != 0) { + return result; + } + return require(state.currentTime() == 2.0); +} + +int analysis_exposes_name_through_base_api() { + RecordingAnalysis analysis; + const fesa::analysis::Analysis& base = analysis; + + return require(base.name()[0] == 'r'); +} + +int analysis_can_be_deleted_through_base_pointer() { + std::unique_ptr analysis = std::make_unique(); + + return require(analysis->name()[0] == 'r'); +} + +} // namespace + +int run_analysis_base_tests() { + if (const int result = derived_analysis_runs_through_base_api(); result != 0) { + return result; + } + if (const int result = analysis_exposes_name_through_base_api(); result != 0) { + return result; + } + if (const int result = analysis_can_be_deleted_through_base_pointer(); result != 0) { + return result; + } + return 0; +} diff --git a/tests/analysis/analysis_test_main.cpp b/tests/analysis/analysis_test_main.cpp new file mode 100644 index 0000000..f7bd2d8 --- /dev/null +++ b/tests/analysis/analysis_test_main.cpp @@ -0,0 +1,12 @@ +int run_analysis_base_tests(); +int run_analysis_state_tests(); + +int main() { + if (const int result = run_analysis_state_tests(); result != 0) { + return result; + } + if (const int result = run_analysis_base_tests(); result != 0) { + return result; + } + return 0; +} diff --git a/tests/core/analysis_state_test.cpp b/tests/core/analysis_state_test.cpp new file mode 100644 index 0000000..ea96134 --- /dev/null +++ b/tests/core/analysis_state_test.cpp @@ -0,0 +1,203 @@ +#include "fesa/core/AnalysisState.hpp" + +#include +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +template +int require_throws(Function&& function) { + try { + function(); + } catch (const Exception&) { + return 0; + } catch (...) { + return 1; + } + return 1; +} + +int default_state_has_no_dofs() { + const fesa::core::AnalysisState state; + + if (const int result = require(state.dofCount() == 0); result != 0) { + return result; + } + if (const int result = require(state.displacement().empty()); result != 0) { + return result; + } + if (const int result = require(state.externalForce().empty()); result != 0) { + return result; + } + if (const int result = require(state.internalForce().empty()); result != 0) { + return result; + } + if (const int result = require(state.residual().empty()); result != 0) { + return result; + } + return require(state.reaction().empty()); +} + +int sized_state_initializes_zero_vectors() { + const fesa::core::AnalysisState state{3}; + + if (const int result = require(state.dofCount() == 3); result != 0) { + return result; + } + for (const auto* vector : { + &state.displacement(), + &state.externalForce(), + &state.internalForce(), + &state.residual(), + &state.reaction(), + }) { + if (const int result = require(vector->size() == state.dofCount()); result != 0) { + return result; + } + for (const double value : *vector) { + if (const int result = require(value == 0.0); result != 0) { + return result; + } + } + } + return 0; +} + +int setters_replace_matching_size_vectors() { + fesa::core::AnalysisState state{2}; + + state.setDisplacement({1.0, 2.0}); + state.setExternalForce({3.0, 4.0}); + state.setInternalForce({5.0, 6.0}); + state.setResidual({7.0, 8.0}); + state.setReaction({9.0, 10.0}); + + if (const int result = require(state.displacement()[0] == 1.0 && state.displacement()[1] == 2.0); result != 0) { + return result; + } + if (const int result = require(state.externalForce()[0] == 3.0 && state.externalForce()[1] == 4.0); result != 0) { + return result; + } + if (const int result = require(state.internalForce()[0] == 5.0 && state.internalForce()[1] == 6.0); result != 0) { + return result; + } + if (const int result = require(state.residual()[0] == 7.0 && state.residual()[1] == 8.0); result != 0) { + return result; + } + return require(state.reaction()[0] == 9.0 && state.reaction()[1] == 10.0); +} + +int mismatched_setter_preserves_existing_vector() { + fesa::core::AnalysisState state{2}; + state.setDisplacement({1.0, 2.0}); + + if (const int result = require_throws([&state]() { + state.setDisplacement({3.0}); + }); + result != 0) { + return result; + } + + return require(state.displacement()[0] == 1.0 && state.displacement()[1] == 2.0); +} + +int resize_resets_all_vectors_to_zero() { + fesa::core::AnalysisState state{2}; + state.setDisplacement({1.0, 2.0}); + state.setExternalForce({3.0, 4.0}); + + state.resize(3); + + if (const int result = require(state.dofCount() == 3); result != 0) { + return result; + } + for (const auto* vector : { + &state.displacement(), + &state.externalForce(), + &state.internalForce(), + &state.residual(), + &state.reaction(), + }) { + for (const double value : *vector) { + if (const int result = require(value == 0.0); result != 0) { + return result; + } + } + } + return 0; +} + +int clear_forces_preserves_displacement() { + fesa::core::AnalysisState state{2}; + state.setDisplacement({1.0, 2.0}); + state.setExternalForce({3.0, 4.0}); + state.setInternalForce({5.0, 6.0}); + state.setResidual({7.0, 8.0}); + state.setReaction({9.0, 10.0}); + + state.clearForces(); + + if (const int result = require(state.displacement()[0] == 1.0 && state.displacement()[1] == 2.0); result != 0) { + return result; + } + for (const auto* vector : { + &state.externalForce(), + &state.internalForce(), + &state.residual(), + &state.reaction(), + }) { + for (const double value : *vector) { + if (const int result = require(value == 0.0); result != 0) { + return result; + } + } + } + return 0; +} + +int counters_can_be_updated() { + fesa::core::AnalysisState state; + + state.setCurrentTime(1.25); + state.setIncrementIndex(3); + state.setIterationIndex(5); + + if (const int result = require(state.currentTime() == 1.25); result != 0) { + return result; + } + if (const int result = require(state.incrementIndex() == 3); result != 0) { + return result; + } + return require(state.iterationIndex() == 5); +} + +} // namespace + +int run_analysis_state_tests() { + if (const int result = default_state_has_no_dofs(); result != 0) { + return result; + } + if (const int result = sized_state_initializes_zero_vectors(); result != 0) { + return result; + } + if (const int result = setters_replace_matching_size_vectors(); result != 0) { + return result; + } + if (const int result = mismatched_setter_preserves_existing_vector(); result != 0) { + return result; + } + if (const int result = resize_resets_all_vectors_to_zero(); result != 0) { + return result; + } + if (const int result = clear_forces_preserves_displacement(); result != 0) { + return result; + } + if (const int result = counters_can_be_updated(); result != 0) { + return result; + } + return 0; +}