feat: add domain model foundation

This commit is contained in:
김경종
2026-06-08 16:40:04 +09:00
parent e4e2f57808
commit fdeac602f4
38 changed files with 2685 additions and 5 deletions
+33
View File
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.20)
project(FESA LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
add_library(fesa_core STATIC
src/core/Domain.cpp
)
target_include_directories(fesa_core PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
enable_testing()
add_executable(fesa_domain_tests
tests/core/boundary_condition_test.cpp
tests/core/domain_bootstrap_test.cpp
tests/core/domain_storage_test.cpp
tests/core/element_definition_test.cpp
tests/core/load_definition_test.cpp
tests/core/material_definition_test.cpp
tests/core/model_types_test.cpp
tests/core/node_test.cpp
tests/core/property_definition_test.cpp
tests/core/step_definition_test.cpp
)
target_link_libraries(fesa_domain_tests PRIVATE fesa_core)
add_test(NAME domain.bootstrap COMMAND fesa_domain_tests)
set_tests_properties(domain.bootstrap PROPERTIES LABELS "domain;core")
+11 -1
View File
@@ -8,6 +8,7 @@
- Multi-Agent coordination files live under `docs/`: `docs/PLAN.md`, `docs/PROGRESS.md`, and `docs/WORKNOTE.md`. - Multi-Agent coordination files live under `docs/`: `docs/PLAN.md`, `docs/PROGRESS.md`, and `docs/WORKNOTE.md`.
- Project plan/design documents live under `docs/project-plan/`. - Project plan/design documents live under `docs/project-plan/`.
- Common per-run AI Agent rules live in `docs/AGENT_RULES.md`. - Common per-run AI Agent rules live in `docs/AGENT_RULES.md`.
- Domain model foundation phase exists under `phases/domain-model-foundation/` and the first C++/MSVC CMake/CTest core implementation slice has been added.
## Completed ## Completed
- Defined the nine-step solver development workflow. - Defined the nine-step solver development workflow.
@@ -23,9 +24,14 @@
- Documented the canonical git remote `https://teagit.mimi1011.synology.me/baram2584/FESADev.git`, default remote `origin`, shared baseline branch `dev`, and `codex/<short-task-name>` work branch convention in `AGENTS.md`, `docs/AGENT_RULES.md`, and `docs/PLAN.md`. - Documented the canonical git remote `https://teagit.mimi1011.synology.me/baram2584/FESADev.git`, default remote `origin`, shared baseline branch `dev`, and `codex/<short-task-name>` work branch convention in `AGENTS.md`, `docs/AGENT_RULES.md`, and `docs/PLAN.md`.
- Confirmed local HDF5 `2.1.1` installation at `C:\Program Files\HDF_Group\HDF5\2.1.1` and recorded the `HDF5_ROOT`/`HDF5_DIR` discovery policy in architecture, agent, skill, and handoff documentation. - Confirmed local HDF5 `2.1.1` installation at `C:\Program Files\HDF_Group\HDF5\2.1.1` and recorded the `HDF5_ROOT`/`HDF5_DIR` discovery policy in architecture, agent, skill, and handoff documentation.
- Recorded the standing user instruction in `AGENTS.md` that Agent-authored work should be validated, committed, and pushed to `origin` after completion. - Recorded the standing user instruction in `AGENTS.md` that Agent-authored work should be validated, committed, and pushed to `origin` after completion.
- Created `docs/implementation-plans/domain-model-foundation-implementation-plan.md`.
- Added root CMake/CTest bootstrap for `fesa_core` and `fesa_domain_tests`.
- Implemented `Domain` model-definition storage for nodes, MITC4 element definitions, linear elastic material definitions, shell properties, node sets, element sets, boundary conditions, nodal loads, and linear static step definitions.
- Added Domain foundation C++ tests under `tests/core/`.
- Fixed `scripts/validate_workspace.py` so CMake/CTest validation can use the default CMake install path when CMake is not on the shell PATH.
## In Progress ## In Progress
- Ready for the next stage: new solver feature requirements analysis for `mitc4-linear-static-shell`. - Ready for the next upstream MITC4 stage: new solver feature requirements analysis for `mitc4-linear-static-shell`.
## Next Tasks ## Next Tasks
1. Create `docs/requirements/mitc4-linear-static-shell.md`. 1. Create `docs/requirements/mitc4-linear-static-shell.md`.
@@ -36,6 +42,10 @@
6. Create `docs/implementation-plans/mitc4-linear-static-shell-implementation-plan.md`. 6. Create `docs/implementation-plans/mitc4-linear-static-shell-implementation-plan.md`.
## Last Validation ## Last Validation
- 2026-06-08: After Domain model foundation implementation, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully.
- 2026-06-08: After Domain model foundation implementation, `python scripts/validate_workspace.py` configured CMake with Visual Studio 17 2022 x64, built Debug targets, ran CTest, and passed.
- 2026-06-08: After Domain model foundation implementation, `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` passed. 1 domain/core test executable ran successfully.
- 2026-06-08: After Domain model foundation implementation, `git diff --check` passed.
- 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `python -m unittest discover -s scripts -p "test_*.py"` passed. 85 tests ran successfully. - 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `python -m unittest discover -s scripts -p "test_*.py"` passed. 85 tests ran successfully.
- 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `python scripts/validate_workspace.py` passed through the expected no-op path because no root `CMakeLists.txt` exists yet. - 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `python scripts/validate_workspace.py` passed through the expected no-op path because no root `CMakeLists.txt` exists yet.
- 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `git diff --check` passed with only Git line-ending normalization warnings. - 2026-06-08: After recording the standing commit/push instruction in `AGENTS.md`, `git diff --check` passed with only Git line-ending normalization warnings.
+3
View File
@@ -13,6 +13,9 @@ During planning, standard checks for `HDF5_ROOT`, `HDF5_DIR`, `h5dump`, and obvi
### 2026-06-08 - HDF5 Installed Under HDF_Group ### 2026-06-08 - HDF5 Installed Under HDF_Group
The local HDF5 install was later confirmed at `C:\Program Files\HDF_Group\HDF5\2.1.1`, and `h5dump.exe -V` reports `2.1.1`. Future CMake work should prefer `HDF5_ROOT=C:\Program Files\HDF_Group\HDF5\2.1.1` or `HDF5_DIR=C:\Program Files\HDF_Group\HDF5\2.1.1\cmake`. Keep the old 2026-06-05 note as historical context for stale sessions, but do not treat HDF5 as currently missing on this machine. The local HDF5 install was later confirmed at `C:\Program Files\HDF_Group\HDF5\2.1.1`, and `h5dump.exe -V` reports `2.1.1`. Future CMake work should prefer `HDF5_ROOT=C:\Program Files\HDF_Group\HDF5\2.1.1` or `HDF5_DIR=C:\Program Files\HDF_Group\HDF5\2.1.1\cmake`. Keep the old 2026-06-05 note as historical context for stale sessions, but do not treat HDF5 as currently missing on this machine.
### 2026-06-08 - CMake Installed But Not On Shell PATH
`cmake.exe` exists at `C:\Program Files\CMake\bin\cmake.exe`, but the Codex shell did not resolve bare `cmake`. `scripts/validate_workspace.py` was updated to resolve bare `cmake` and `ctest` validation commands to the known CMake install path when needed. Use `python scripts/validate_workspace.py` as the canonical validation path, or call `C:\Program Files\CMake\bin\cmake.exe` / `ctest.exe` explicitly for manual commands.
### 2026-06-05 - Validation Currently Has No CMake Project ### 2026-06-05 - Validation Currently Has No CMake Project
`python scripts/validate_workspace.py` currently exits successfully through the no-op path because there is no root `CMakeLists.txt`. This is expected until the C++ solver project is bootstrapped. Once CMake is introduced, validation must configure, build, and run CTest. `python scripts/validate_workspace.py` currently exits successfully through the no-op path because there is no root `CMakeLists.txt`. This is expected until the C++ solver project is bootstrapped. Once CMake is introduced, validation must configure, build, and run CTest.
@@ -0,0 +1,74 @@
# Domain Model Foundation Build/Test Report
## Metadata
- feature_id: domain-model-foundation
- source_implementation_report: N/A
- source_implementation_plan: docs/implementation-plans/domain-model-foundation-implementation-plan.md
- status: pass-for-next-implementation-stage
- owner_agent: build-test-executor-agent
- date: 2026-06-08
## Execution Environment
- os: Windows
- generator: Visual Studio 17 2022
- platform: x64
- config: Debug
- build_dir: build/msvc-debug
- active_override_env_vars: none
- command_discovery_path: default CMake/MSVC x64 Debug
## Command Log Summary
| order | command | exit_code | duration | stdout_stderr_tail |
| --- | --- | --- | --- | --- |
| 1 | `git status --short --branch` | 0 | <1s | branch `codex/domain-model-foundation`; only Agent-authored changes observed |
| 2 | `git remote -v` | 0 | <1s | `origin` points to `https://teagit.mimi1011.synology.me/baram2584/FESADev.git` |
| 3 | `python -m unittest discover -s scripts -p "test_*.py"` | 0 | <1s | 89 tests passed |
| 4 | `python scripts/validate_workspace.py` | 0 | ~8s | configure, build, and CTest passed; validation succeeded |
| 5 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` | 0 | <1s | 1 domain/core CTest passed |
| 6 | `git diff --check` | 0 | <1s | no whitespace errors |
## Validation Results
| validation_stage | result | evidence |
| --- | --- | --- |
| harness self-test | pass | `python -m unittest discover -s scripts -p "test_*.py"` ran 89 tests successfully |
| configure | pass | CMake generated `build/msvc-debug` with Visual Studio 17 2022 x64 |
| build | pass | `fesa_core.lib` and `fesa_domain_tests.exe` built in Debug |
| CTest | pass | `domain.bootstrap` passed with labels `domain;core` |
| feature-specific tests | pass | Domain model storage tests are compiled into `fesa_domain_tests` |
## Failure Classification
- classification: N/A
- primary_failure: N/A
- first_failed_command: N/A
- evidence_tail: validation succeeded
## Failed Test Inventory
| test_name | label | command | failure_summary |
| --- | --- | --- | --- |
| N/A | N/A | N/A | N/A |
## Handoff Recommendation
| target_agent | reason | required_input |
| --- | --- | --- |
| Implementation Agent | Continue with parser/factory or AnalysisModel work after upstream contracts are ready | Domain headers, tests, and implementation plan |
| Build/Test Executor Agent | Re-run after downstream C++ changes | validation commands in this report |
| Reference Verification Agent | Not ready; this phase produces no solver HDF5 result | N/A |
## No-Change Assertion
- source_files_modified: true
- test_files_modified: true
- cmake_files_modified: true
- reference_artifacts_modified: false
- tolerance_policies_modified: false
- notes: This phase added C++ core model-definition storage only. It did not run reference solvers, generate reference artifacts, or change tolerance policy.
## Open Issues
- MITC4 requirements, research, formulation, I/O contract, reference models, and full implementation plan remain separate upstream stages.
- Parser-to-Domain factory API is intentionally deferred.
@@ -0,0 +1,190 @@
# Domain Model Foundation Implementation Plan
## Metadata
- feature_id: domain-model-foundation
- source_requirement: AGENTS.md; docs/ARCHITECTURE.md; docs/ADR.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 input-model ownership rules
- source_reference_models: N/A for this architecture foundation slice
- status: ready-for-implementation
- owner_agent: implementation-planning-agent
- date: 2026-06-08
## Readiness Check
| input | required_status | observed_status | decision |
| --- | --- | --- | --- |
| architecture | Domain, AnalysisModel, AnalysisState, DofManager boundaries documented | documented in docs/ARCHITECTURE.md and docs/ADR.md | proceed |
| requirements | Domain ownership rules documented | documented in AGENTS.md and docs/ARCHITECTURE.md | proceed |
| formulation | not required for model-container storage | N/A | proceed |
| numerical_review | not required for model-container storage | N/A | proceed |
| io_definition | parser contract not required; semantic storage boundary documented | sufficient for Domain foundation | proceed |
| reference_models | not required because this slice produces no solver result | N/A | proceed |
## Implementation Scope
- included_behavior: C++17 core model-definition storage for nodes, element definitions, material definitions, shell property definitions, sets, boundary conditions, nodal loads, and linear static step definitions.
- included_behavior: CMake/CTest bootstrap for MSVC x64 Debug validation.
- included_behavior: deterministic id validation and const retrieval APIs for stored model definitions.
- excluded_behavior: MITC4 stiffness, shape functions, Jacobian checks, assembly, solver, reaction recovery, HDF5 output, Abaqus parser, MKL, TBB, and reference comparison.
- non_goals: numerical correctness claims, release readiness, Abaqus/Nastran execution, and stored reference artifact generation.
## Domain Contract
`Domain` owns parsed model definitions only. It preserves the model that an input parser or factory layer creates and should be treated as immutable by analysis code after parsing/building is complete.
Included model definitions:
- nodes
- element definitions
- material definitions
- shell property definitions
- node sets
- element sets
- boundary conditions
- nodal loads
- analysis step definitions
Excluded state:
- equation ids
- sparse matrix structure or values
- reduced or full displacement vectors
- residual vectors
- reaction vectors
- current time
- increment counters
- nonlinear iteration counters
- element integration point state
- MKL, TBB, or HDF5 handles
Boundary responsibilities:
- `DofManager` owns active DOF discovery, constrained/free DOF mapping, equation numbering, sparse pattern connectivity, and full-vector reconstruction.
- `AnalysisModel` owns the step-local execution view over active domain objects. It references `Domain` objects by id or stable reference and does not copy the whole domain.
- `AnalysisState` owns mutable solution, residual, load vector, time/increment, and future element state.
Common data rules:
- All ids use signed 64-bit storage.
- Node DOF order is `U1, U2, U3, UR1, UR2, UR3`.
- Coordinates and scalar physical values use `double`.
- Units are user-consistent and are not enforced or converted by `Domain`.
- Duplicate ids fail with `std::invalid_argument`.
- Insertions that reference missing required objects fail with `std::invalid_argument`.
- Direct lookup of a missing object fails with `std::out_of_range`.
- Optional `find*` lookup APIs return `nullptr` for missing objects.
- Retrieval APIs return const references or const pointers.
## Work Breakdown
| task_id | order | purpose | upstream_trace | depends_on | expected_test_first |
| --- | --- | --- | --- | --- | --- |
| TASK-001 | 1 | Bootstrap CMake, core target, and CTest for Domain tests | ADR-002 | none | TEST-001 |
| TASK-002 | 2 | Define signed 64-bit id aliases and six-DOF ordering constants | ADR-014; AGENTS.md MITC4 scope | TASK-001 | TEST-002 |
| TASK-003 | 3 | Add `Node` and node storage/retrieval in `Domain` | ADR-010; docs/ARCHITECTURE.md Domain section | TASK-002 | TEST-003 |
| TASK-004 | 4 | Add MITC4 element definition storage without element computation | ADR-004; docs/ARCHITECTURE.md Core Runtime Objects | TASK-003 | TEST-004 |
| TASK-005 | 5 | Add material, shell property, node set, and element set storage | docs/ARCHITECTURE.md Domain included data | TASK-004 | TEST-005 |
| TASK-006 | 6 | Add boundary condition, nodal load, and linear static step definition storage | ADR-012; docs/ARCHITECTURE.md Domain included data | TASK-005 | TEST-006 |
| TASK-007 | 7 | Lock down Domain invariants and failed-insert stability | ADR-010; docs/AGENT_RULES.md boundaries | TASK-006 | TEST-007 |
| TASK-008 | 8 | Record build/test evidence and handoff status | docs/build-test-reports/README.md | TASK-007 | TEST-008 |
## TDD Test Plan
| test_id | order | test_type | red_condition | green_condition | linked_task | command |
| --- | --- | --- | --- | --- | --- | --- |
| TEST-001 | 1 | CTest bootstrap | `domain.bootstrap` is not registered or cannot run | `domain.bootstrap` passes under MSVC Debug | TASK-001 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain.bootstrap` |
| TEST-002 | 2 | unit | `fesa/core/ModelTypes.hpp` is missing | id aliases are 64-bit and DOF ordinals are 0..5 | TASK-002 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-003 | 3 | unit | `Node` and `Domain` node APIs are missing | add/find/direct lookup/duplicate node tests pass | TASK-003 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-004 | 4 | unit | element definition APIs are missing | element add/find/direct lookup/connectivity/missing-node tests pass | TASK-004 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-005 | 5 | unit | material, property, and set APIs are missing | material/property/set validation tests pass | TASK-005 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-006 | 6 | unit | boundary, load, and step APIs are missing | BC/load/step storage tests pass | TASK-006 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-007 | 7 | unit | failed insertion mutates Domain or mutable access leaks | invariant and failed-insert stability tests pass | TASK-007 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` |
| TEST-008 | 8 | validation | build/test evidence is missing | build/test report records passing validation or classified failure | TASK-008 | `python scripts/validate_workspace.py` |
## CMake/CTest Plan
- target_candidates: `fesa_core`, `fesa_domain_tests`
- add_test_needs: `domain.bootstrap` initially, then Domain behavior tests under the same `domain` CTest filter.
- labels: `domain`, `core`
- msvc_config: Debug
- expected_feature_command: `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain`
- workspace_validation: `python scripts/validate_workspace.py`
## Candidate Files and Ownership
| file_candidate | purpose | owner_boundary | notes |
| --- | --- | --- | --- |
| `CMakeLists.txt` | root CMake project, core library, CTest registration | build/test bootstrap | MSVC x64 Debug baseline |
| `include/fesa/core/ModelTypes.hpp` | id aliases and DOF constants | core model definitions | no solver state |
| `include/fesa/core/Node.hpp` | node id and coordinate definition | core model definitions | no displacement storage |
| `include/fesa/core/ElementDefinition.hpp` | element id, type, connectivity, property reference | core model definitions | no stiffness behavior |
| `include/fesa/core/MaterialDefinition.hpp` | linear elastic material input definition | core model definitions | no constitutive matrix |
| `include/fesa/core/PropertyDefinition.hpp` | shell property input definition | core model definitions | no section stiffness |
| `include/fesa/core/BoundaryCondition.hpp` | prescribed DOF value definition | core model definitions | no matrix application |
| `include/fesa/core/LoadDefinition.hpp` | nodal load definition | core model definitions | no global vector assembly |
| `include/fesa/core/StepDefinition.hpp` | linear static step definition | core model definitions | no analysis driver |
| `include/fesa/core/Domain.hpp` | Domain public storage/retrieval API | core model definitions | const retrieval only |
| `src/core/Domain.cpp` | Domain validation and storage implementation | core model definitions | no external dependencies |
| `tests/core/domain_bootstrap_test.cpp` | CTest bootstrap executable | test | created before production code |
| `tests/core/model_types_test.cpp` | id and DOF tests | test | optional separate executable |
| `tests/core/domain_storage_test.cpp` | Domain behavior tests | test | required before production changes |
## Data Flow Contract
1. A future Abaqus parser and factory/registry layer creates semantic model definitions.
2. `Domain` stores those definitions and validates duplicate ids and missing required references.
3. A future `AnalysisModel` builds a step-local execution view from `Domain` ids or stable references.
4. A future `DofManager` derives active DOFs and equation ids outside `Domain`.
5. A future `AnalysisState` stores solution and iteration state outside `Domain`.
## Acceptance Traceability Matrix
| requirement_id | task_id | test_id | reference_model_id | acceptance_criterion | status |
| --- | --- | --- | --- | --- | --- |
| DOM-REQ-001 Domain owns parsed model definitions | TASK-003 to TASK-006 | TEST-003 to TEST-006 | N/A | storage and const retrieval tests pass | draft |
| DOM-REQ-002 Domain excludes solver state | TASK-007 | TEST-007 | N/A | invariant tests and member review pass | draft |
| DOM-REQ-003 ids use signed 64-bit storage | TASK-002 | TEST-002 | N/A | `sizeof(Id) == 8` test passes | draft |
| DOM-REQ-004 node DOF order is fixed | TASK-002 | TEST-002 | N/A | DOF ordinal test passes | draft |
| DOM-REQ-005 duplicate ids are rejected | TASK-003 to TASK-006 | TEST-003 to TEST-006 | N/A | duplicate tests throw `std::invalid_argument` | draft |
| DOM-REQ-006 missing required references are rejected | TASK-004 to TASK-006 | TEST-004 to TEST-006 | N/A | missing-reference tests throw `std::invalid_argument` | draft |
| DOM-REQ-007 direct missing lookup is deterministic | TASK-003 to TASK-007 | TEST-003 to TEST-007 | N/A | direct missing lookup throws `std::out_of_range` | draft |
| DOM-REQ-008 validation uses MSVC CMake/CTest | TASK-001; TASK-008 | TEST-001; TEST-008 | N/A | configure/build/CTest evidence recorded | draft |
## Validation Commands
```powershell
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
```
## Risks and Downstream Handoff
### Implementation Agent
- Keep each storage layer separate and test-first.
- Do not add numerical behavior while implementing model definitions.
- Preserve `Domain` as a model container, not a solver-state object.
### Build/Test Executor Agent
- Use Visual Studio 17 2022, x64, Debug, and `build/msvc-debug`.
- After `CMakeLists.txt` exists, `python scripts/validate_workspace.py` must configure, build, and run CTest instead of taking the no-CMake path.
### Correction Agent
- Implementation-owned failures are expected to be compile or unit-test failures in `fesa_core` or `fesa_domain_tests`.
- Upstream-contract failures include requests to store equation ids, displacements, reaction vectors, or integration point state inside `Domain`.
### Reference Verification Agent
- No reference verification is required for this phase.
- This phase produces no HDF5 result and consumes no reference artifacts.
## Open Issues
- The full MITC4 requirements, formulation, I/O definition, and reference model artifacts are still separate upstream stages.
- The parser-to-Domain factory API is intentionally deferred until the Abaqus input subset phase.
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
namespace fesa::core {
class BoundaryCondition {
public:
BoundaryCondition(NodeId node_id, Dof dof, double value);
NodeId nodeId() const noexcept;
Dof dof() const noexcept;
double value() const noexcept;
private:
NodeId node_id_;
Dof dof_;
double value_;
};
} // namespace fesa::core
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "fesa/core/BoundaryCondition.hpp"
#include "fesa/core/ElementDefinition.hpp"
#include "fesa/core/LoadDefinition.hpp"
#include "fesa/core/MaterialDefinition.hpp"
#include "fesa/core/ModelTypes.hpp"
#include "fesa/core/Node.hpp"
#include "fesa/core/PropertyDefinition.hpp"
#include "fesa/core/StepDefinition.hpp"
#include <cstddef>
#include <string>
#include <unordered_map>
#include <vector>
namespace fesa::core {
class Domain {
public:
void addNode(Node node);
void addElement(ElementDefinition element);
void addMaterial(LinearElasticMaterialDefinition material);
void addShellProperty(ShellPropertyDefinition property);
void addNodeSet(std::string name, std::vector<NodeId> node_ids);
void addElementSet(std::string name, std::vector<ElementId> element_ids);
std::size_t addBoundaryCondition(BoundaryCondition condition);
std::size_t addNodalLoad(NodalLoadDefinition load);
void addStep(LinearStaticStepDefinition step);
const Node* findNode(NodeId id) const noexcept;
const Node& node(NodeId id) const;
std::size_t nodeCount() const noexcept;
const ElementDefinition* findElement(ElementId id) const noexcept;
const ElementDefinition& element(ElementId id) const;
std::size_t elementCount() const noexcept;
const LinearElasticMaterialDefinition* findMaterial(MaterialId id) const noexcept;
const LinearElasticMaterialDefinition& material(MaterialId id) const;
std::size_t materialCount() const noexcept;
const ShellPropertyDefinition* findShellProperty(PropertyId id) const noexcept;
const ShellPropertyDefinition& shellProperty(PropertyId id) const;
std::size_t shellPropertyCount() const noexcept;
const std::vector<NodeId>* findNodeSet(const std::string& name) const noexcept;
const std::vector<NodeId>& nodeSet(const std::string& name) const;
std::size_t nodeSetCount() const noexcept;
const std::vector<ElementId>* findElementSet(const std::string& name) const noexcept;
const std::vector<ElementId>& elementSet(const std::string& name) const;
std::size_t elementSetCount() const noexcept;
const BoundaryCondition& boundaryCondition(std::size_t index) const;
std::size_t boundaryConditionCount() const noexcept;
const NodalLoadDefinition& nodalLoad(std::size_t index) const;
std::size_t nodalLoadCount() const noexcept;
const LinearStaticStepDefinition* findStep(StepId id) const noexcept;
const LinearStaticStepDefinition& step(StepId id) const;
std::size_t stepCount() const noexcept;
private:
std::unordered_map<NodeId, Node> nodes_;
std::unordered_map<ElementId, ElementDefinition> elements_;
std::unordered_map<MaterialId, LinearElasticMaterialDefinition> materials_;
std::unordered_map<PropertyId, ShellPropertyDefinition> shell_properties_;
std::unordered_map<std::string, std::vector<NodeId>> node_sets_;
std::unordered_map<std::string, std::vector<ElementId>> element_sets_;
std::vector<BoundaryCondition> boundary_conditions_;
std::vector<NodalLoadDefinition> nodal_loads_;
std::unordered_map<StepId, LinearStaticStepDefinition> steps_;
};
} // namespace fesa::core
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
#include <array>
namespace fesa::core {
enum class ElementType {
Mitc4
};
class ElementDefinition {
public:
ElementDefinition(
ElementId id,
ElementType type,
std::array<NodeId, 4> connectivity,
PropertyId property_id);
ElementId id() const noexcept;
ElementType type() const noexcept;
const std::array<NodeId, 4>& connectivity() const noexcept;
PropertyId propertyId() const noexcept;
private:
ElementId id_;
ElementType type_;
std::array<NodeId, 4> connectivity_;
PropertyId property_id_;
};
} // namespace fesa::core
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
namespace fesa::core {
class NodalLoadDefinition {
public:
NodalLoadDefinition(NodeId node_id, Dof dof, double value);
NodeId nodeId() const noexcept;
Dof dof() const noexcept;
double value() const noexcept;
private:
NodeId node_id_;
Dof dof_;
double value_;
};
} // namespace fesa::core
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
namespace fesa::core {
class LinearElasticMaterialDefinition {
public:
LinearElasticMaterialDefinition(MaterialId id, double young_modulus, double poisson_ratio);
MaterialId id() const noexcept;
double youngModulus() const noexcept;
double poissonRatio() const noexcept;
private:
MaterialId id_;
double young_modulus_;
double poisson_ratio_;
};
} // namespace fesa::core
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstddef>
#include <cstdint>
namespace fesa::core {
using Id = std::int64_t;
using NodeId = Id;
using ElementId = Id;
using MaterialId = Id;
using PropertyId = Id;
using StepId = Id;
enum class Dof : std::uint8_t {
U1 = 0,
U2 = 1,
U3 = 2,
UR1 = 3,
UR2 = 4,
UR3 = 5
};
constexpr std::size_t kDofPerNode = 6;
} // namespace fesa::core
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
#include <array>
namespace fesa::core {
class Node {
public:
Node(NodeId id, double x, double y, double z);
NodeId id() const noexcept;
double x() const noexcept;
double y() const noexcept;
double z() const noexcept;
const std::array<double, 3>& coordinates() const noexcept;
private:
NodeId id_;
std::array<double, 3> coordinates_;
};
} // namespace fesa::core
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
namespace fesa::core {
class ShellPropertyDefinition {
public:
ShellPropertyDefinition(PropertyId id, MaterialId material_id, double thickness);
PropertyId id() const noexcept;
MaterialId materialId() const noexcept;
double thickness() const noexcept;
private:
PropertyId id_;
MaterialId material_id_;
double thickness_;
};
} // namespace fesa::core
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include "fesa/core/ModelTypes.hpp"
#include <cstddef>
#include <string>
#include <vector>
namespace fesa::core {
class LinearStaticStepDefinition {
public:
LinearStaticStepDefinition(
StepId id,
std::string name,
std::vector<std::size_t> boundary_condition_indices,
std::vector<std::size_t> load_indices);
StepId id() const noexcept;
const std::string& name() const noexcept;
const std::vector<std::size_t>& boundaryConditionIndices() const noexcept;
const std::vector<std::size_t>& loadIndices() const noexcept;
private:
StepId id_;
std::string name_;
std::vector<std::size_t> boundary_condition_indices_;
std::vector<std::size_t> load_indices_;
};
} // namespace fesa::core
+60
View File
@@ -0,0 +1,60 @@
{
"project": "FESA Structural Solver",
"phase": "domain-model-foundation",
"steps": [
{
"step": 0,
"name": "domain-contract",
"status": "completed",
"summary": "Created the Domain model foundation implementation plan with ownership boundaries, TDD tasks, and validation commands."
},
{
"step": 1,
"name": "cmake-test-bootstrap",
"status": "completed",
"summary": "Added root CMake/CTest bootstrap for fesa_domain_tests and fixed validation CMake path resolution on Windows."
},
{
"step": 2,
"name": "core-id-types",
"status": "completed",
"summary": "Added ModelTypes.hpp with 64-bit ids and fixed six-DOF ordering covered by C++ tests."
},
{
"step": 3,
"name": "node-and-domain-storage",
"status": "completed",
"summary": "Implemented Node and Domain node storage with duplicate and missing lookup tests."
},
{
"step": 4,
"name": "element-definition-storage",
"status": "completed",
"summary": "Added MITC4 element definition storage with connectivity preservation and missing-node validation tests."
},
{
"step": 5,
"name": "model-attribute-storage",
"status": "completed",
"summary": "Added material, shell property, node set, and element set storage with duplicate and missing-reference tests."
},
{
"step": 6,
"name": "boundary-load-step-storage",
"status": "completed",
"summary": "Added boundary condition, nodal load, and linear static step storage with missing-reference tests."
},
{
"step": 7,
"name": "domain-invariants",
"status": "completed",
"summary": "Locked down const retrieval and failed-insert count stability with invariant tests."
},
{
"step": 8,
"name": "validation-report-handoff",
"status": "completed",
"summary": "Recorded build/test evidence, updated handoff docs, and marked the phase completed."
}
]
}
+77
View File
@@ -0,0 +1,77 @@
# Step 0: domain-contract
## 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/README.md`
## Task
Create `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`.
The document must define the first C++ implementation slice for the `Domain` model container. It must explicitly state:
- `Domain` owns parsed model definitions only.
- Included model definitions: nodes, elements, materials, shell properties, node sets, element sets, boundary conditions, nodal loads, and analysis step definitions.
- Excluded state: equation ids, sparse matrix state, displacement vectors, residuals, reactions, current time, iteration counters, and element integration point state.
- `DofManager` owns equation numbering.
- `AnalysisModel` owns step-local execution views.
- `AnalysisState` owns mutable solution and iteration state.
- All ids use signed 64-bit storage.
- Node DOF order is `U1, U2, U3, UR1, UR2, UR3`.
- Units are user-consistent and not enforced by `Domain`.
- No Abaqus, Nastran, reference solver, HDF5 result, MKL, or TBB behavior is implemented in this phase.
Use the implementation-plan README template. Set:
- `feature_id: domain-model-foundation`
- `status: ready-for-implementation`
- `owner_agent: implementation-planning-agent`
- `date: 2026-06-08`
Include a work breakdown and TDD test plan for the following downstream steps:
1. CMake/CTest bootstrap.
2. Core id and DOF types.
3. Node and node storage.
4. Element definition storage.
5. Material, property, and set storage.
6. Boundary condition, nodal load, and step storage.
7. Domain invariant tests.
8. Validation report and handoff.
## 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
```
The current repository may still take the no-CMake informational path until Step 1 creates CMake files.
Update `/phases/domain-model-foundation/index.json` step 0:
- On success: `"status": "completed"` and a one-line `"summary"`.
- On repeated failure: `"status": "error"` and a concrete `"error_message"`.
- On user decision needed: `"status": "blocked"` and a concrete `"blocked_reason"`.
## Do Not
- Do not create C++ headers, sources, tests, or CMake files in this step.
- Do not change requirements, formulation, I/O contracts, reference artifacts, or tolerance policy.
- Do not run Abaqus, Nastran, or any reference solver.
+70
View File
@@ -0,0 +1,70 @@
# Step 1: cmake-test-bootstrap
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/phases/domain-model-foundation/index.json`
- `/phases/domain-model-foundation/step0.md`
## Task
Create the minimum C++17/MSVC/CMake/CTest project structure needed to run Domain tests.
Allowed files:
- Create `/CMakeLists.txt`
- Create `/include/fesa/core/.gitkeep` only if needed to preserve the directory before headers exist
- Create `/src/core/.gitkeep` only if needed to preserve the directory before sources exist
- Create `/tests/core/domain_bootstrap_test.cpp`
CMake requirements:
- `cmake_minimum_required(VERSION 3.20)`
- Project name: `FESA`
- Language: CXX
- Set C++ standard to 17 and require it.
- Create interface or library target `fesa_core`.
- Add include directory `/include`.
- Enable testing.
- Create test executable `fesa_domain_tests`.
- Register CTest name `domain.bootstrap`.
- Apply CTest labels `domain;core`.
## Tests To Write First
Write `/tests/core/domain_bootstrap_test.cpp` before adding any production C++ code. It should be a minimal self-contained executable with `main()` returning 0 only when the C++ test harness is running.
Example intended behavior:
```cpp
int main() {
return 0;
}
```
This step does not implement Domain behavior yet. The purpose is to prove the MSVC/CTest path exists.
## Acceptance Criteria
Run:
```powershell
cmake -S . -B build/msvc-debug -G "Visual Studio 17 2022" -A x64
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain.bootstrap
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 1 with `completed`, `error`, or `blocked`.
## Do Not
- Do not add Domain, Node, Element, solver, parser, MKL, TBB, or HDF5 implementation in this step.
- Do not add JavaScript, TypeScript, npm, or non-MSVC fallback tooling.
+76
View File
@@ -0,0 +1,76 @@
# Step 2: core-id-types
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/CMakeLists.txt`
- `/tests/core/domain_bootstrap_test.cpp`
## Task
Introduce core id aliases and DOF ordering constants.
Allowed files:
- Create `/include/fesa/core/ModelTypes.hpp`
- Modify `/tests/core/domain_bootstrap_test.cpp` or create `/tests/core/model_types_test.cpp`
- Modify `/CMakeLists.txt` only to register the new test file if a separate test executable is created
Required API:
```cpp
namespace fesa::core {
using Id = std::int64_t;
using NodeId = Id;
using ElementId = Id;
using MaterialId = Id;
using PropertyId = Id;
using StepId = Id;
enum class Dof : std::uint8_t {
U1 = 0,
U2 = 1,
U3 = 2,
UR1 = 3,
UR2 = 4,
UR3 = 5
};
constexpr std::size_t kDofPerNode = 6;
}
```
## Tests To Write First
Write a failing C++ test that includes `fesa/core/ModelTypes.hpp` before creating the header.
The test must verify:
- `sizeof(fesa::core::Id) == 8`
- `kDofPerNode == 6`
- `Dof::U1` through `Dof::UR3` have ordinal values 0 through 5
Run the targeted CTest and verify it fails because the header is missing. Then implement the header.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 2 with `completed`, `error`, or `blocked`.
## Do Not
- Do not add model storage in this step.
- Do not add equation numbering or solver state.
+63
View File
@@ -0,0 +1,63 @@
# Step 3: node-and-domain-storage
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/include/fesa/core/ModelTypes.hpp`
- `/CMakeLists.txt`
## Task
Implement the first useful `Node` and `Domain` slice.
Allowed files:
- Create `/include/fesa/core/Node.hpp`
- Create `/include/fesa/core/Domain.hpp`
- Create `/src/core/Domain.cpp`
- Create or modify `/tests/core/domain_storage_test.cpp`
- Modify `/CMakeLists.txt` to compile the source and register the test
Required behavior:
- `Node` stores `NodeId` and three coordinates as `double`.
- `Domain::addNode(Node)` inserts a node.
- Duplicate node ids throw `std::invalid_argument`.
- `Domain::findNode(NodeId)` returns a pointer or `nullptr`.
- `Domain::node(NodeId)` returns a const reference or throws `std::out_of_range`.
- `Domain::nodeCount()` returns the stored node count.
- `Domain` must not expose mutable node references.
## Tests To Write First
Write failing tests before creating production headers/sources:
- Add and retrieve one node by id.
- Missing node returns `nullptr` from `findNode`.
- Missing node throws from `node`.
- Duplicate node id throws.
Run the targeted CTest and verify failure due to missing API. Then implement the minimal code.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 3 with `completed`, `error`, or `blocked`.
## Do Not
- Do not add elements, materials, properties, sets, loads, or steps yet.
- Do not store equation ids, displacement vectors, residuals, reactions, or solver state.
+65
View File
@@ -0,0 +1,65 @@
# Step 4: element-definition-storage
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/include/fesa/core/Domain.hpp`
- `/include/fesa/core/Node.hpp`
- `/include/fesa/core/ModelTypes.hpp`
- `/tests/core/domain_storage_test.cpp`
## Task
Add element definition storage without implementing element stiffness or MITC4 formulation.
Allowed files:
- Create `/include/fesa/core/ElementDefinition.hpp`
- Modify `/include/fesa/core/Domain.hpp`
- Modify `/src/core/Domain.cpp`
- Modify `/tests/core/domain_storage_test.cpp`
Required behavior:
- `ElementDefinition` stores `ElementId`, type enum/string for `MITC4`, four `NodeId` connectivity entries, and a `PropertyId`.
- `Domain::addElement(ElementDefinition)` inserts an element.
- Duplicate element ids throw `std::invalid_argument`.
- Element connectivity must contain exactly four node ids for the MITC4 definition.
- Adding an element with a missing node id throws `std::invalid_argument`.
- `Domain::findElement(ElementId)` returns pointer or `nullptr`.
- `Domain::element(ElementId)` returns const reference or throws `std::out_of_range`.
- `Domain::elementCount()` returns the stored element count.
## Tests To Write First
Write failing tests before production changes:
- Add an element with four existing nodes and retrieve connectivity in order.
- Duplicate element id throws.
- Element referencing a missing node throws.
- Missing element lookup follows the `findElement` and `element` contracts.
Run targeted CTest and verify RED before implementation.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 4 with `completed`, `error`, or `blocked`.
## Do Not
- Do not implement element stiffness, Jacobians, shape functions, sparse assembly, or solver behavior.
- Do not add parser behavior.
+66
View File
@@ -0,0 +1,66 @@
# Step 5: model-attribute-storage
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/include/fesa/core/Domain.hpp`
- `/include/fesa/core/ElementDefinition.hpp`
- `/tests/core/domain_storage_test.cpp`
## Task
Add minimal material, shell property, node set, and element set storage.
Allowed files:
- Create `/include/fesa/core/MaterialDefinition.hpp`
- Create `/include/fesa/core/PropertyDefinition.hpp`
- Modify `/include/fesa/core/Domain.hpp`
- Modify `/src/core/Domain.cpp`
- Modify `/tests/core/domain_storage_test.cpp`
Required behavior:
- `LinearElasticMaterialDefinition` stores material id, Young's modulus, and Poisson's ratio.
- `ShellPropertyDefinition` stores property id, material id, and thickness.
- `Domain` stores node sets and element sets by string name.
- Duplicate material ids, property ids, or set names throw `std::invalid_argument`.
- Adding a shell property with missing material id throws `std::invalid_argument`.
- Node sets can only reference existing nodes.
- Element sets can only reference existing elements.
- All retrieval APIs return const data.
## Tests To Write First
Write failing tests before production changes:
- Add material and shell property, then retrieve them.
- Duplicate material/property id throws.
- Shell property referencing missing material throws.
- Node set stores existing node ids in input order.
- Element set stores existing element ids in input order.
- Sets referencing missing ids throw.
Run targeted CTest and verify RED before implementation.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 5 with `completed`, `error`, or `blocked`.
## Do Not
- Do not implement constitutive matrices, shell section stiffness, units conversion, or parser behavior.
+66
View File
@@ -0,0 +1,66 @@
# Step 6: boundary-load-step-storage
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/include/fesa/core/Domain.hpp`
- `/include/fesa/core/ModelTypes.hpp`
- `/tests/core/domain_storage_test.cpp`
## Task
Add boundary condition, nodal load, and analysis step definition storage.
Allowed files:
- Create `/include/fesa/core/BoundaryCondition.hpp`
- Create `/include/fesa/core/LoadDefinition.hpp`
- Create `/include/fesa/core/StepDefinition.hpp`
- Modify `/include/fesa/core/Domain.hpp`
- Modify `/src/core/Domain.cpp`
- Modify `/tests/core/domain_storage_test.cpp`
Required behavior:
- A boundary condition stores node id, constrained `Dof`, and prescribed value.
- A nodal load stores node id, `Dof`, and value.
- A linear static step definition stores step id, name, boundary condition indices or ids, and load indices or ids.
- Adding BC/load for a missing node throws `std::invalid_argument`.
- Duplicate step ids throw `std::invalid_argument`.
- Stored BCs, loads, and steps are returned as const data.
## Tests To Write First
Write failing tests before production changes:
- Add and retrieve a boundary condition on an existing node.
- Add and retrieve a nodal load on an existing node.
- Missing-node BC/load throws.
- Add a linear static step referencing stored BC/load entries.
- Duplicate step id throws.
Run targeted CTest and verify RED before implementation.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 6 with `completed`, `error`, or `blocked`.
## Do Not
- Do not apply boundary conditions to matrices.
- Do not compute reactions.
- Do not add solver state, displacements, equation ids, or sparse matrix behavior.
+61
View File
@@ -0,0 +1,61 @@
# Step 7: domain-invariants
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/ADR.md`
- `/docs/ARCHITECTURE.md`
- `/docs/implementation-plans/domain-model-foundation-implementation-plan.md`
- `/include/fesa/core/Domain.hpp`
- `/src/core/Domain.cpp`
- `/tests/core/domain_storage_test.cpp`
## Task
Add tests and small API cleanup to lock down `Domain` invariants.
Allowed files:
- Modify `/include/fesa/core/Domain.hpp`
- Modify `/src/core/Domain.cpp`
- Modify `/tests/core/domain_storage_test.cpp`
- Modify `/docs/implementation-plans/domain-model-foundation-implementation-plan.md` only if a test traceability table needs correction
Required invariants:
- `Domain` does not expose mutable references to stored objects.
- `Domain` has no equation id, displacement, residual, reaction, current time, iteration, sparse matrix, MKL, TBB, or HDF5 members.
- Retrieval does not create missing objects implicitly.
- Insert operations preserve input ids and ordering where the API promises ordering.
- Error types remain deterministic: duplicate ids use `std::invalid_argument`; missing required references use `std::invalid_argument`; direct missing lookup uses `std::out_of_range`.
## Tests To Write First
Write failing compile/runtime tests before API cleanup:
- A const `Domain` can retrieve model definitions.
- Missing direct lookup throws without changing counts.
- Failed insert due to missing reference does not change the relevant count.
- Existing node, element, material, property, set, BC, load, and step counts remain stable after failed inserts.
Run targeted CTest and verify RED before implementation.
## Acceptance Criteria
Run:
```powershell
cmake --build build/msvc-debug --config Debug
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
python scripts/validate_workspace.py
```
Update `/phases/domain-model-foundation/index.json` step 7 with `completed`, `error`, or `blocked`.
## Do Not
- Do not add new solver features beyond invariant enforcement.
- Do not refactor unrelated files.
+63
View File
@@ -0,0 +1,63 @@
# Step 8: validation-report-handoff
## Read First
Read these files before editing:
- `/AGENTS.md`
- `/docs/AGENT_RULES.md`
- `/docs/build-test-reports/README.md`
- `/docs/PROGRESS.md`
- `/docs/WORKNOTE.md`
- `/phases/domain-model-foundation/index.json`
- All Domain files created by previous steps
## Task
Run final verification and write the handoff evidence.
Allowed files:
- Create `/docs/build-test-reports/domain-model-foundation-build-test.md`
- Modify `/docs/PROGRESS.md`
- Modify `/docs/WORKNOTE.md` only if a failed approach, environment issue, or trap was encountered
- Modify `/phases/domain-model-foundation/index.json`
- Modify `/phases/index.json`
The build/test report must include:
- command log summary with exit codes
- command discovery path
- configure/build/CTest status
- failure classification or `N/A`
- failed test inventory or `N/A`
- no-change assertion for reference artifacts and tolerance policies
## Tests To Write First
This is a validation/reporting step. Do not write new C++ production behavior.
## Acceptance Criteria
Run:
```powershell
git status --short --branch
git remote -v
python -m unittest discover -s scripts -p "test_*.py"
python scripts/validate_workspace.py
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain
git diff --check
```
Update:
- `/phases/domain-model-foundation/index.json` step 8 with `completed`, `error`, or `blocked`.
- `/phases/index.json` phase status to `completed` only if all steps are completed.
- `/docs/PROGRESS.md` with the executed validation evidence.
## Do Not
- Do not claim MITC4 implementation, numerical correctness, reference comparison success, or release readiness.
- Do not run Abaqus, Nastran, or any reference solver.
- Do not edit reference artifacts.
+8
View File
@@ -0,0 +1,8 @@
{
"phases": [
{
"dir": "domain-model-foundation",
"status": "completed"
}
]
}
+56 -1
View File
@@ -87,7 +87,62 @@ class ValidateWorkspaceTests(unittest.TestCase):
with patch.object(validate_workspace.shutil, "which", return_value=None): with patch.object(validate_workspace.shutil, "which", return_value=None):
env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"}) env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"})
self.assertTrue(env["PATH"].startswith(str(common_bin))) path_key = "Path" if os.name == "nt" else "PATH"
self.assertTrue(env[path_key].startswith(str(common_bin)))
def test_common_cmake_install_path_updates_existing_windows_path_key(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
env = validate_workspace.validation_environment({"Path": "C:\\Windows\\System32"})
self.assertIn("Path", env)
self.assertNotIn("PATH", env)
self.assertTrue(env["Path"].startswith(str(common_bin)))
def test_common_cmake_install_path_normalizes_uppercase_path_on_windows(self):
if os.name != "nt":
self.skipTest("Windows-specific subprocess environment behavior")
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
env = validate_workspace.validation_environment({"PATH": "C:\\Windows\\System32"})
self.assertIn("Path", env)
self.assertNotIn("PATH", env)
self.assertTrue(env["Path"].startswith(str(common_bin)))
def test_common_cmake_executable_is_used_when_command_tool_is_not_on_path(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "cmake.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
command = validate_workspace.resolve_validation_command("cmake --version")
self.assertEqual(command, f'"{common_bin / "cmake.exe"}" --version')
def test_common_ctest_executable_is_used_when_command_tool_is_not_on_path(self):
validate_workspace = load_validate_workspace()
with tempfile.TemporaryDirectory() as tmp:
common_bin = Path(tmp) / "CMake" / "bin"
common_bin.mkdir(parents=True)
(common_bin / "ctest.exe").write_text("", encoding="utf-8")
with patch.object(validate_workspace, "COMMON_CMAKE_BIN", common_bin):
with patch.object(validate_workspace.shutil, "which", return_value=None):
command = validate_workspace.resolve_validation_command("ctest --version")
self.assertEqual(command, f'"{common_bin / "ctest.exe"}" --version')
if __name__ == "__main__": if __name__ == "__main__":
+31 -3
View File
@@ -88,7 +88,27 @@ def discover_commands(root: Path) -> list[str]:
return load_cmake_commands(root) return load_cmake_commands(root)
def resolve_validation_command(command: str) -> str:
parts = command.split(maxsplit=1)
if not parts:
return command
tool = parts[0].lower()
if tool not in {"cmake", "ctest"}:
return command
if shutil.which(tool) is not None:
return command
exe = COMMON_CMAKE_BIN / f"{tool}.exe"
if not exe.exists():
return command
suffix = f" {parts[1]}" if len(parts) > 1 else ""
return f'"{exe}"{suffix}'
def run_command(command: str, root: Path) -> subprocess.CompletedProcess: def run_command(command: str, root: Path) -> subprocess.CompletedProcess:
command = resolve_validation_command(command)
return subprocess.run( return subprocess.run(
command, command,
cwd=root, cwd=root,
@@ -103,17 +123,25 @@ def run_command(command: str, root: Path) -> subprocess.CompletedProcess:
def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]: def validation_environment(base_env: os._Environ | dict[str, str]) -> dict[str, str]:
env = dict(base_env) env = dict(base_env)
if shutil.which("cmake") is not None: path_key = "Path" if os.name == "nt" else "PATH"
path_values = []
for key in list(env):
if key.lower() == "path":
path_values.append(env.pop(key))
current_path = os.pathsep.join(part for part in path_values if part)
env[path_key] = current_path
if shutil.which("cmake", path=current_path) is not None:
return env return env
cmake_exe = COMMON_CMAKE_BIN / "cmake.exe" cmake_exe = COMMON_CMAKE_BIN / "cmake.exe"
if not cmake_exe.exists(): if not cmake_exe.exists():
return env return env
current_path = env.get("PATH", "")
paths = [part for part in current_path.split(os.pathsep) if part] paths = [part for part in current_path.split(os.pathsep) if part]
common_bin_text = str(COMMON_CMAKE_BIN) common_bin_text = str(COMMON_CMAKE_BIN)
if not any(part.lower() == common_bin_text.lower() for part in paths): if not any(part.lower() == common_bin_text.lower() for part in paths):
env["PATH"] = common_bin_text + (os.pathsep + current_path if current_path else "") env[path_key] = common_bin_text + (os.pathsep + current_path if current_path else "")
return env return env
+381
View File
@@ -0,0 +1,381 @@
#include "fesa/core/Domain.hpp"
#include <stdexcept>
#include <utility>
namespace fesa::core {
BoundaryCondition::BoundaryCondition(NodeId node_id, Dof dof, double value)
: node_id_(node_id), dof_(dof), value_(value) {}
NodeId BoundaryCondition::nodeId() const noexcept {
return node_id_;
}
Dof BoundaryCondition::dof() const noexcept {
return dof_;
}
double BoundaryCondition::value() const noexcept {
return value_;
}
ElementDefinition::ElementDefinition(
ElementId id,
ElementType type,
std::array<NodeId, 4> connectivity,
PropertyId property_id)
: id_(id), type_(type), connectivity_(connectivity), property_id_(property_id) {}
ElementId ElementDefinition::id() const noexcept {
return id_;
}
ElementType ElementDefinition::type() const noexcept {
return type_;
}
const std::array<NodeId, 4>& ElementDefinition::connectivity() const noexcept {
return connectivity_;
}
PropertyId ElementDefinition::propertyId() const noexcept {
return property_id_;
}
LinearElasticMaterialDefinition::LinearElasticMaterialDefinition(
MaterialId id,
double young_modulus,
double poisson_ratio)
: id_(id), young_modulus_(young_modulus), poisson_ratio_(poisson_ratio) {}
MaterialId LinearElasticMaterialDefinition::id() const noexcept {
return id_;
}
double LinearElasticMaterialDefinition::youngModulus() const noexcept {
return young_modulus_;
}
double LinearElasticMaterialDefinition::poissonRatio() const noexcept {
return poisson_ratio_;
}
LinearStaticStepDefinition::LinearStaticStepDefinition(
StepId id,
std::string name,
std::vector<std::size_t> boundary_condition_indices,
std::vector<std::size_t> load_indices)
: id_(id),
name_(std::move(name)),
boundary_condition_indices_(std::move(boundary_condition_indices)),
load_indices_(std::move(load_indices)) {}
StepId LinearStaticStepDefinition::id() const noexcept {
return id_;
}
const std::string& LinearStaticStepDefinition::name() const noexcept {
return name_;
}
const std::vector<std::size_t>& LinearStaticStepDefinition::boundaryConditionIndices() const noexcept {
return boundary_condition_indices_;
}
const std::vector<std::size_t>& LinearStaticStepDefinition::loadIndices() const noexcept {
return load_indices_;
}
NodalLoadDefinition::NodalLoadDefinition(NodeId node_id, Dof dof, double value)
: node_id_(node_id), dof_(dof), value_(value) {}
NodeId NodalLoadDefinition::nodeId() const noexcept {
return node_id_;
}
Dof NodalLoadDefinition::dof() const noexcept {
return dof_;
}
double NodalLoadDefinition::value() const noexcept {
return value_;
}
Node::Node(NodeId id, double x, double y, double z)
: id_(id), coordinates_{x, y, z} {}
NodeId Node::id() const noexcept {
return id_;
}
double Node::x() const noexcept {
return coordinates_[0];
}
double Node::y() const noexcept {
return coordinates_[1];
}
double Node::z() const noexcept {
return coordinates_[2];
}
const std::array<double, 3>& Node::coordinates() const noexcept {
return coordinates_;
}
ShellPropertyDefinition::ShellPropertyDefinition(
PropertyId id,
MaterialId material_id,
double thickness)
: id_(id), material_id_(material_id), thickness_(thickness) {}
PropertyId ShellPropertyDefinition::id() const noexcept {
return id_;
}
MaterialId ShellPropertyDefinition::materialId() const noexcept {
return material_id_;
}
double ShellPropertyDefinition::thickness() const noexcept {
return thickness_;
}
void Domain::addNode(Node node) {
const NodeId id = node.id();
const auto inserted = nodes_.emplace(id, std::move(node));
if (!inserted.second) {
throw std::invalid_argument("duplicate node id");
}
}
void Domain::addElement(ElementDefinition element) {
const ElementId id = element.id();
if (elements_.find(id) != elements_.end()) {
throw std::invalid_argument("duplicate element id");
}
for (const NodeId node_id : element.connectivity()) {
if (findNode(node_id) == nullptr) {
throw std::invalid_argument("element references missing node id");
}
}
elements_.emplace(id, std::move(element));
}
void Domain::addMaterial(LinearElasticMaterialDefinition material) {
const MaterialId id = material.id();
const auto inserted = materials_.emplace(id, std::move(material));
if (!inserted.second) {
throw std::invalid_argument("duplicate material id");
}
}
void Domain::addShellProperty(ShellPropertyDefinition property) {
const PropertyId id = property.id();
if (shell_properties_.find(id) != shell_properties_.end()) {
throw std::invalid_argument("duplicate shell property id");
}
if (findMaterial(property.materialId()) == nullptr) {
throw std::invalid_argument("shell property references missing material id");
}
shell_properties_.emplace(id, std::move(property));
}
void Domain::addNodeSet(std::string name, std::vector<NodeId> node_ids) {
if (node_sets_.find(name) != node_sets_.end()) {
throw std::invalid_argument("duplicate node set name");
}
for (const NodeId node_id : node_ids) {
if (findNode(node_id) == nullptr) {
throw std::invalid_argument("node set references missing node id");
}
}
node_sets_.emplace(std::move(name), std::move(node_ids));
}
void Domain::addElementSet(std::string name, std::vector<ElementId> element_ids) {
if (element_sets_.find(name) != element_sets_.end()) {
throw std::invalid_argument("duplicate element set name");
}
for (const ElementId element_id : element_ids) {
if (findElement(element_id) == nullptr) {
throw std::invalid_argument("element set references missing element id");
}
}
element_sets_.emplace(std::move(name), std::move(element_ids));
}
std::size_t Domain::addBoundaryCondition(BoundaryCondition condition) {
if (findNode(condition.nodeId()) == nullptr) {
throw std::invalid_argument("boundary condition references missing node id");
}
const std::size_t index = boundary_conditions_.size();
boundary_conditions_.push_back(condition);
return index;
}
std::size_t Domain::addNodalLoad(NodalLoadDefinition load) {
if (findNode(load.nodeId()) == nullptr) {
throw std::invalid_argument("nodal load references missing node id");
}
const std::size_t index = nodal_loads_.size();
nodal_loads_.push_back(load);
return index;
}
void Domain::addStep(LinearStaticStepDefinition step) {
const StepId id = step.id();
if (steps_.find(id) != steps_.end()) {
throw std::invalid_argument("duplicate step id");
}
for (const std::size_t index : step.boundaryConditionIndices()) {
if (index >= boundary_conditions_.size()) {
throw std::invalid_argument("step references missing boundary condition");
}
}
for (const std::size_t index : step.loadIndices()) {
if (index >= nodal_loads_.size()) {
throw std::invalid_argument("step references missing nodal load");
}
}
steps_.emplace(id, std::move(step));
}
const Node* Domain::findNode(NodeId id) const noexcept {
const auto it = nodes_.find(id);
return it == nodes_.end() ? nullptr : &it->second;
}
const Node& Domain::node(NodeId id) const {
const Node* found = findNode(id);
if (found == nullptr) {
throw std::out_of_range("node id not found");
}
return *found;
}
std::size_t Domain::nodeCount() const noexcept {
return nodes_.size();
}
const ElementDefinition* Domain::findElement(ElementId id) const noexcept {
const auto it = elements_.find(id);
return it == elements_.end() ? nullptr : &it->second;
}
const ElementDefinition& Domain::element(ElementId id) const {
const ElementDefinition* found = findElement(id);
if (found == nullptr) {
throw std::out_of_range("element id not found");
}
return *found;
}
std::size_t Domain::elementCount() const noexcept {
return elements_.size();
}
const LinearElasticMaterialDefinition* Domain::findMaterial(MaterialId id) const noexcept {
const auto it = materials_.find(id);
return it == materials_.end() ? nullptr : &it->second;
}
const LinearElasticMaterialDefinition& Domain::material(MaterialId id) const {
const LinearElasticMaterialDefinition* found = findMaterial(id);
if (found == nullptr) {
throw std::out_of_range("material id not found");
}
return *found;
}
std::size_t Domain::materialCount() const noexcept {
return materials_.size();
}
const ShellPropertyDefinition* Domain::findShellProperty(PropertyId id) const noexcept {
const auto it = shell_properties_.find(id);
return it == shell_properties_.end() ? nullptr : &it->second;
}
const ShellPropertyDefinition& Domain::shellProperty(PropertyId id) const {
const ShellPropertyDefinition* found = findShellProperty(id);
if (found == nullptr) {
throw std::out_of_range("shell property id not found");
}
return *found;
}
std::size_t Domain::shellPropertyCount() const noexcept {
return shell_properties_.size();
}
const std::vector<NodeId>* Domain::findNodeSet(const std::string& name) const noexcept {
const auto it = node_sets_.find(name);
return it == node_sets_.end() ? nullptr : &it->second;
}
const std::vector<NodeId>& Domain::nodeSet(const std::string& name) const {
const std::vector<NodeId>* found = findNodeSet(name);
if (found == nullptr) {
throw std::out_of_range("node set not found");
}
return *found;
}
std::size_t Domain::nodeSetCount() const noexcept {
return node_sets_.size();
}
const std::vector<ElementId>* Domain::findElementSet(const std::string& name) const noexcept {
const auto it = element_sets_.find(name);
return it == element_sets_.end() ? nullptr : &it->second;
}
const std::vector<ElementId>& Domain::elementSet(const std::string& name) const {
const std::vector<ElementId>* found = findElementSet(name);
if (found == nullptr) {
throw std::out_of_range("element set not found");
}
return *found;
}
std::size_t Domain::elementSetCount() const noexcept {
return element_sets_.size();
}
const BoundaryCondition& Domain::boundaryCondition(std::size_t index) const {
return boundary_conditions_.at(index);
}
std::size_t Domain::boundaryConditionCount() const noexcept {
return boundary_conditions_.size();
}
const NodalLoadDefinition& Domain::nodalLoad(std::size_t index) const {
return nodal_loads_.at(index);
}
std::size_t Domain::nodalLoadCount() const noexcept {
return nodal_loads_.size();
}
const LinearStaticStepDefinition* Domain::findStep(StepId id) const noexcept {
const auto it = steps_.find(id);
return it == steps_.end() ? nullptr : &it->second;
}
const LinearStaticStepDefinition& Domain::step(StepId id) const {
const LinearStaticStepDefinition* found = findStep(id);
if (found == nullptr) {
throw std::out_of_range("step id not found");
}
return *found;
}
std::size_t Domain::stepCount() const noexcept {
return steps_.size();
}
} // namespace fesa::core
+25
View File
@@ -0,0 +1,25 @@
#include "fesa/core/BoundaryCondition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_boundary_condition_tests() {
const fesa::core::BoundaryCondition condition{1, fesa::core::Dof::UR3, 0.25};
if (const int result = require(condition.nodeId() == 1); result != 0) {
return result;
}
if (const int result = require(condition.dof() == fesa::core::Dof::UR3); result != 0) {
return result;
}
if (const int result = require(condition.value() == 0.25); result != 0) {
return result;
}
return 0;
}
+40
View File
@@ -0,0 +1,40 @@
int run_boundary_condition_tests();
int run_domain_storage_tests();
int run_element_definition_tests();
int run_load_definition_tests();
int run_material_definition_tests();
int run_model_types_tests();
int run_node_tests();
int run_property_definition_tests();
int run_step_definition_tests();
int main() {
if (const int result = run_model_types_tests(); result != 0) {
return result;
}
if (const int result = run_node_tests(); result != 0) {
return result;
}
if (const int result = run_element_definition_tests(); result != 0) {
return result;
}
if (const int result = run_material_definition_tests(); result != 0) {
return result;
}
if (const int result = run_property_definition_tests(); result != 0) {
return result;
}
if (const int result = run_boundary_condition_tests(); result != 0) {
return result;
}
if (const int result = run_load_definition_tests(); result != 0) {
return result;
}
if (const int result = run_step_definition_tests(); result != 0) {
return result;
}
if (const int result = run_domain_storage_tests(); result != 0) {
return result;
}
return 0;
}
+655
View File
@@ -0,0 +1,655 @@
#include "fesa/core/BoundaryCondition.hpp"
#include "fesa/core/Domain.hpp"
#include "fesa/core/ElementDefinition.hpp"
#include "fesa/core/LoadDefinition.hpp"
#include "fesa/core/MaterialDefinition.hpp"
#include "fesa/core/Node.hpp"
#include "fesa/core/PropertyDefinition.hpp"
#include "fesa/core/StepDefinition.hpp"
#include <stdexcept>
#include <type_traits>
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
template <typename Exception, typename Function>
int require_throws(Function&& function) {
try {
function();
} catch (const Exception&) {
return 0;
} catch (...) {
return 1;
}
return 1;
}
int add_and_retrieve_node_by_id() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{10, 1.0, 2.0, 3.0});
if (const int result = require(domain.nodeCount() == 1); result != 0) {
return result;
}
const fesa::core::Node* found = domain.findNode(10);
if (const int result = require(found != nullptr); result != 0) {
return result;
}
if (const int result = require(found->id() == 10); result != 0) {
return result;
}
if (const int result = require(found->x() == 1.0); result != 0) {
return result;
}
if (const int result = require(found->y() == 2.0); result != 0) {
return result;
}
if (const int result = require(found->z() == 3.0); result != 0) {
return result;
}
const fesa::core::Node& direct = domain.node(10);
if (const int result = require(direct.id() == 10); result != 0) {
return result;
}
return 0;
}
int missing_node_lookup_contracts() {
const fesa::core::Domain domain;
if (const int result = require(domain.findNode(99) == nullptr); result != 0) {
return result;
}
return require_throws<std::out_of_range>([&domain]() {
(void)domain.node(99);
});
}
int duplicate_node_id_throws() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{10, 0.0, 0.0, 0.0});
return require_throws<std::invalid_argument>([&domain]() {
domain.addNode(fesa::core::Node{10, 1.0, 0.0, 0.0});
});
}
void add_four_nodes(fesa::core::Domain& domain) {
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
domain.addNode(fesa::core::Node{2, 1.0, 0.0, 0.0});
domain.addNode(fesa::core::Node{3, 1.0, 1.0, 0.0});
domain.addNode(fesa::core::Node{4, 0.0, 1.0, 0.0});
}
int add_and_retrieve_element_by_id() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
if (const int result = require(domain.elementCount() == 1); result != 0) {
return result;
}
const fesa::core::ElementDefinition* found = domain.findElement(100);
if (const int result = require(found != nullptr); result != 0) {
return result;
}
if (const int result = require(found->id() == 100); result != 0) {
return result;
}
if (const int result = require(found->type() == fesa::core::ElementType::Mitc4); result != 0) {
return result;
}
if (const int result = require(found->propertyId() == 500); result != 0) {
return result;
}
if (const int result = require(found->connectivity()[0] == 1); result != 0) {
return result;
}
if (const int result = require(found->connectivity()[1] == 2); result != 0) {
return result;
}
if (const int result = require(found->connectivity()[2] == 3); result != 0) {
return result;
}
if (const int result = require(found->connectivity()[3] == 4); result != 0) {
return result;
}
const fesa::core::ElementDefinition& direct = domain.element(100);
return require(direct.id() == 100);
}
int duplicate_element_id_throws() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
return require_throws<std::invalid_argument>([&domain]() {
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
});
}
int element_referencing_missing_node_throws() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
domain.addNode(fesa::core::Node{2, 1.0, 0.0, 0.0});
domain.addNode(fesa::core::Node{3, 1.0, 1.0, 0.0});
return require_throws<std::invalid_argument>([&domain]() {
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
});
}
int missing_element_lookup_contracts() {
const fesa::core::Domain domain;
if (const int result = require(domain.findElement(404) == nullptr); result != 0) {
return result;
}
return require_throws<std::out_of_range>([&domain]() {
(void)domain.element(404);
});
}
void add_material_and_property(fesa::core::Domain& domain) {
domain.addMaterial(fesa::core::LinearElasticMaterialDefinition{700, 210.0, 0.3});
domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.01});
}
int add_and_retrieve_material_and_property() {
fesa::core::Domain domain;
add_material_and_property(domain);
if (const int result = require(domain.materialCount() == 1); result != 0) {
return result;
}
if (const int result = require(domain.shellPropertyCount() == 1); result != 0) {
return result;
}
const fesa::core::LinearElasticMaterialDefinition* material = domain.findMaterial(700);
if (const int result = require(material != nullptr); result != 0) {
return result;
}
if (const int result = require(material->youngModulus() == 210.0); result != 0) {
return result;
}
if (const int result = require(material->poissonRatio() == 0.3); result != 0) {
return result;
}
const fesa::core::ShellPropertyDefinition* property = domain.findShellProperty(500);
if (const int result = require(property != nullptr); result != 0) {
return result;
}
if (const int result = require(property->materialId() == 700); result != 0) {
return result;
}
if (const int result = require(property->thickness() == 0.01); result != 0) {
return result;
}
if (const int result = require(domain.material(700).id() == 700); result != 0) {
return result;
}
return require(domain.shellProperty(500).id() == 500);
}
int duplicate_material_and_property_ids_throw() {
fesa::core::Domain domain;
add_material_and_property(domain);
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addMaterial(fesa::core::LinearElasticMaterialDefinition{700, 100.0, 0.25});
});
result != 0) {
return result;
}
return require_throws<std::invalid_argument>([&domain]() {
domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.02});
});
}
int shell_property_referencing_missing_material_throws() {
fesa::core::Domain domain;
return require_throws<std::invalid_argument>([&domain]() {
domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.01});
});
}
int add_and_retrieve_sets() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
domain.addNodeSet("left-edge", {1, 4});
domain.addElementSet("shells", {100});
const auto* node_set = domain.findNodeSet("left-edge");
if (const int result = require(node_set != nullptr); result != 0) {
return result;
}
if (const int result = require(node_set->size() == 2); result != 0) {
return result;
}
if (const int result = require((*node_set)[0] == 1); result != 0) {
return result;
}
if (const int result = require((*node_set)[1] == 4); result != 0) {
return result;
}
const auto* element_set = domain.findElementSet("shells");
if (const int result = require(element_set != nullptr); result != 0) {
return result;
}
if (const int result = require(element_set->size() == 1); result != 0) {
return result;
}
if (const int result = require((*element_set)[0] == 100); result != 0) {
return result;
}
if (const int result = require(domain.nodeSetCount() == 1); result != 0) {
return result;
}
return require(domain.elementSetCount() == 1);
}
int duplicate_set_names_throw() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
domain.addNodeSet("left-edge", {1, 4});
domain.addElementSet("shells", {100});
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addNodeSet("left-edge", {1});
});
result != 0) {
return result;
}
return require_throws<std::invalid_argument>([&domain]() {
domain.addElementSet("shells", {100});
});
}
int sets_referencing_missing_ids_throw() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addNodeSet("bad-nodes", {1, 99});
});
result != 0) {
return result;
}
return require_throws<std::invalid_argument>([&domain]() {
domain.addElementSet("bad-elements", {100, 404});
});
}
int add_and_retrieve_boundary_condition() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
const std::size_t index = domain.addBoundaryCondition(
fesa::core::BoundaryCondition{1, fesa::core::Dof::U1, 0.0});
if (const int result = require(index == 0); result != 0) {
return result;
}
if (const int result = require(domain.boundaryConditionCount() == 1); result != 0) {
return result;
}
const fesa::core::BoundaryCondition& condition = domain.boundaryCondition(index);
if (const int result = require(condition.nodeId() == 1); result != 0) {
return result;
}
if (const int result = require(condition.dof() == fesa::core::Dof::U1); result != 0) {
return result;
}
return require(condition.value() == 0.0);
}
int add_and_retrieve_nodal_load() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
const std::size_t index = domain.addNodalLoad(
fesa::core::NodalLoadDefinition{1, fesa::core::Dof::U3, -100.0});
if (const int result = require(index == 0); result != 0) {
return result;
}
if (const int result = require(domain.nodalLoadCount() == 1); result != 0) {
return result;
}
const fesa::core::NodalLoadDefinition& load = domain.nodalLoad(index);
if (const int result = require(load.nodeId() == 1); result != 0) {
return result;
}
if (const int result = require(load.dof() == fesa::core::Dof::U3); result != 0) {
return result;
}
return require(load.value() == -100.0);
}
int missing_node_boundary_condition_and_load_throw() {
fesa::core::Domain domain;
if (const int result = require_throws<std::invalid_argument>([&domain]() {
(void)domain.addBoundaryCondition(
fesa::core::BoundaryCondition{99, fesa::core::Dof::U1, 0.0});
});
result != 0) {
return result;
}
return require_throws<std::invalid_argument>([&domain]() {
(void)domain.addNodalLoad(
fesa::core::NodalLoadDefinition{99, fesa::core::Dof::U3, -100.0});
});
}
int add_and_retrieve_linear_static_step() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
const std::size_t bc = domain.addBoundaryCondition(
fesa::core::BoundaryCondition{1, fesa::core::Dof::U1, 0.0});
const std::size_t load = domain.addNodalLoad(
fesa::core::NodalLoadDefinition{1, fesa::core::Dof::U3, -100.0});
domain.addStep(fesa::core::LinearStaticStepDefinition{1, "load-step", {bc}, {load}});
if (const int result = require(domain.stepCount() == 1); result != 0) {
return result;
}
const fesa::core::LinearStaticStepDefinition* found = domain.findStep(1);
if (const int result = require(found != nullptr); result != 0) {
return result;
}
if (const int result = require(found->id() == 1); result != 0) {
return result;
}
if (const int result = require(found->name() == "load-step"); result != 0) {
return result;
}
if (const int result = require(found->boundaryConditionIndices().size() == 1); result != 0) {
return result;
}
if (const int result = require(found->boundaryConditionIndices()[0] == bc); result != 0) {
return result;
}
if (const int result = require(found->loadIndices().size() == 1); result != 0) {
return result;
}
return require(found->loadIndices()[0] == load);
}
int duplicate_and_invalid_step_references_throw() {
fesa::core::Domain domain;
domain.addNode(fesa::core::Node{1, 0.0, 0.0, 0.0});
const std::size_t bc = domain.addBoundaryCondition(
fesa::core::BoundaryCondition{1, fesa::core::Dof::U1, 0.0});
const std::size_t load = domain.addNodalLoad(
fesa::core::NodalLoadDefinition{1, fesa::core::Dof::U3, -100.0});
domain.addStep(fesa::core::LinearStaticStepDefinition{1, "load-step", {bc}, {load}});
if (const int result = require_throws<std::invalid_argument>([&domain, bc, load]() {
domain.addStep(fesa::core::LinearStaticStepDefinition{1, "duplicate", {bc}, {load}});
});
result != 0) {
return result;
}
if (const int result = require_throws<std::invalid_argument>([&domain, load]() {
domain.addStep(fesa::core::LinearStaticStepDefinition{2, "bad-bc", {99}, {load}});
});
result != 0) {
return result;
}
return require_throws<std::invalid_argument>([&domain, bc]() {
domain.addStep(fesa::core::LinearStaticStepDefinition{2, "bad-load", {bc}, {99}});
});
}
int const_domain_retrieval_returns_const_model_data() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addMaterial(fesa::core::LinearElasticMaterialDefinition{700, 210.0, 0.3});
domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.01});
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
domain.addNodeSet("left-edge", {1, 4});
domain.addElementSet("shells", {100});
const std::size_t bc = domain.addBoundaryCondition(
fesa::core::BoundaryCondition{1, fesa::core::Dof::U1, 0.0});
const std::size_t load = domain.addNodalLoad(
fesa::core::NodalLoadDefinition{1, fesa::core::Dof::U3, -100.0});
domain.addStep(fesa::core::LinearStaticStepDefinition{1, "load-step", {bc}, {load}});
const fesa::core::Domain& const_domain = domain;
if (const int result = require((std::is_same<decltype(const_domain.node(1)), const fesa::core::Node&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.element(100)), const fesa::core::ElementDefinition&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.material(700)), const fesa::core::LinearElasticMaterialDefinition&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.shellProperty(500)), const fesa::core::ShellPropertyDefinition&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.nodeSet("left-edge")), const std::vector<fesa::core::NodeId>&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.elementSet("shells")), const std::vector<fesa::core::ElementId>&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.boundaryCondition(0)), const fesa::core::BoundaryCondition&>::value));
result != 0) {
return result;
}
if (const int result = require((std::is_same<decltype(const_domain.nodalLoad(0)), const fesa::core::NodalLoadDefinition&>::value));
result != 0) {
return result;
}
return require((std::is_same<decltype(const_domain.step(1)), const fesa::core::LinearStaticStepDefinition&>::value));
}
int failed_inserts_do_not_mutate_counts() {
fesa::core::Domain domain;
add_four_nodes(domain);
domain.addMaterial(fesa::core::LinearElasticMaterialDefinition{700, 210.0, 0.3});
domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.01});
domain.addElement(fesa::core::ElementDefinition{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500});
domain.addNodeSet("left-edge", {1, 4});
domain.addElementSet("shells", {100});
const std::size_t bc = domain.addBoundaryCondition(
fesa::core::BoundaryCondition{1, fesa::core::Dof::U1, 0.0});
const std::size_t load = domain.addNodalLoad(
fesa::core::NodalLoadDefinition{1, fesa::core::Dof::U3, -100.0});
domain.addStep(fesa::core::LinearStaticStepDefinition{1, "load-step", {bc}, {load}});
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addElement(fesa::core::ElementDefinition{
101,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 99},
500});
});
result != 0) {
return result;
}
if (const int result = require(domain.elementCount() == 1); result != 0) {
return result;
}
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addShellProperty(fesa::core::ShellPropertyDefinition{501, 404, 0.01});
});
result != 0) {
return result;
}
if (const int result = require(domain.shellPropertyCount() == 1); result != 0) {
return result;
}
if (const int result = require_throws<std::invalid_argument>([&domain]() {
domain.addNodeSet("bad-nodes", {1, 99});
});
result != 0) {
return result;
}
if (const int result = require(domain.nodeSetCount() == 1); result != 0) {
return result;
}
if (const int result = require_throws<std::invalid_argument>([&domain]() {
(void)domain.addBoundaryCondition(
fesa::core::BoundaryCondition{99, fesa::core::Dof::U1, 0.0});
});
result != 0) {
return result;
}
if (const int result = require(domain.boundaryConditionCount() == 1); result != 0) {
return result;
}
if (const int result = require_throws<std::invalid_argument>([&domain, bc]() {
domain.addStep(fesa::core::LinearStaticStepDefinition{2, "bad-load", {bc}, {99}});
});
result != 0) {
return result;
}
return require(domain.stepCount() == 1);
}
} // namespace
int run_domain_storage_tests() {
if (const int result = add_and_retrieve_node_by_id(); result != 0) {
return result;
}
if (const int result = missing_node_lookup_contracts(); result != 0) {
return result;
}
if (const int result = duplicate_node_id_throws(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_element_by_id(); result != 0) {
return result;
}
if (const int result = duplicate_element_id_throws(); result != 0) {
return result;
}
if (const int result = element_referencing_missing_node_throws(); result != 0) {
return result;
}
if (const int result = missing_element_lookup_contracts(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_material_and_property(); result != 0) {
return result;
}
if (const int result = duplicate_material_and_property_ids_throw(); result != 0) {
return result;
}
if (const int result = shell_property_referencing_missing_material_throws(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_sets(); result != 0) {
return result;
}
if (const int result = duplicate_set_names_throw(); result != 0) {
return result;
}
if (const int result = sets_referencing_missing_ids_throw(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_boundary_condition(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_nodal_load(); result != 0) {
return result;
}
if (const int result = missing_node_boundary_condition_and_load_throw(); result != 0) {
return result;
}
if (const int result = add_and_retrieve_linear_static_step(); result != 0) {
return result;
}
if (const int result = duplicate_and_invalid_step_references_throw(); result != 0) {
return result;
}
if (const int result = const_domain_retrieval_returns_const_model_data(); result != 0) {
return result;
}
if (const int result = failed_inserts_do_not_mutate_counts(); result != 0) {
return result;
}
return 0;
}
+41
View File
@@ -0,0 +1,41 @@
#include "fesa/core/ElementDefinition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_element_definition_tests() {
const fesa::core::ElementDefinition element{
100,
fesa::core::ElementType::Mitc4,
{1, 2, 3, 4},
500};
if (const int result = require(element.id() == 100); result != 0) {
return result;
}
if (const int result = require(element.type() == fesa::core::ElementType::Mitc4); result != 0) {
return result;
}
if (const int result = require(element.connectivity()[0] == 1); result != 0) {
return result;
}
if (const int result = require(element.connectivity()[1] == 2); result != 0) {
return result;
}
if (const int result = require(element.connectivity()[2] == 3); result != 0) {
return result;
}
if (const int result = require(element.connectivity()[3] == 4); result != 0) {
return result;
}
if (const int result = require(element.propertyId() == 500); result != 0) {
return result;
}
return 0;
}
+25
View File
@@ -0,0 +1,25 @@
#include "fesa/core/LoadDefinition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_load_definition_tests() {
const fesa::core::NodalLoadDefinition load{1, fesa::core::Dof::U3, -100.0};
if (const int result = require(load.nodeId() == 1); result != 0) {
return result;
}
if (const int result = require(load.dof() == fesa::core::Dof::U3); result != 0) {
return result;
}
if (const int result = require(load.value() == -100.0); result != 0) {
return result;
}
return 0;
}
+25
View File
@@ -0,0 +1,25 @@
#include "fesa/core/MaterialDefinition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_material_definition_tests() {
const fesa::core::LinearElasticMaterialDefinition material{700, 210.0, 0.3};
if (const int result = require(material.id() == 700); result != 0) {
return result;
}
if (const int result = require(material.youngModulus() == 210.0); result != 0) {
return result;
}
if (const int result = require(material.poissonRatio() == 0.3); result != 0) {
return result;
}
return 0;
}
+46
View File
@@ -0,0 +1,46 @@
#include "fesa/core/ModelTypes.hpp"
#include <cstdint>
#include <type_traits>
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_model_types_tests() {
using namespace fesa::core;
if (const int result = require(sizeof(Id) == 8); result != 0) {
return result;
}
if (const int result = require(std::is_same<Id, std::int64_t>::value); result != 0) {
return result;
}
if (const int result = require(kDofPerNode == 6); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::U1) == 0); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::U2) == 1); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::U3) == 2); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::UR1) == 3); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::UR2) == 4); result != 0) {
return result;
}
if (const int result = require(static_cast<int>(Dof::UR3) == 5); result != 0) {
return result;
}
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
#include "fesa/core/Node.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_node_tests() {
const fesa::core::Node node{42, 1.0, 2.0, 3.0};
if (const int result = require(node.id() == 42); result != 0) {
return result;
}
if (const int result = require(node.x() == 1.0); result != 0) {
return result;
}
if (const int result = require(node.y() == 2.0); result != 0) {
return result;
}
if (const int result = require(node.z() == 3.0); result != 0) {
return result;
}
if (const int result = require(node.coordinates()[0] == 1.0); result != 0) {
return result;
}
if (const int result = require(node.coordinates()[1] == 2.0); result != 0) {
return result;
}
if (const int result = require(node.coordinates()[2] == 3.0); result != 0) {
return result;
}
return 0;
}
+25
View File
@@ -0,0 +1,25 @@
#include "fesa/core/PropertyDefinition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_property_definition_tests() {
const fesa::core::ShellPropertyDefinition property{500, 700, 0.01};
if (const int result = require(property.id() == 500); result != 0) {
return result;
}
if (const int result = require(property.materialId() == 700); result != 0) {
return result;
}
if (const int result = require(property.thickness() == 0.01); result != 0) {
return result;
}
return 0;
}
+37
View File
@@ -0,0 +1,37 @@
#include "fesa/core/StepDefinition.hpp"
namespace {
int require(bool condition) {
return condition ? 0 : 1;
}
} // namespace
int run_step_definition_tests() {
const fesa::core::LinearStaticStepDefinition step{1, "load-step", {0, 2}, {1}};
if (const int result = require(step.id() == 1); result != 0) {
return result;
}
if (const int result = require(step.name() == "load-step"); result != 0) {
return result;
}
if (const int result = require(step.boundaryConditionIndices().size() == 2); result != 0) {
return result;
}
if (const int result = require(step.boundaryConditionIndices()[0] == 0); result != 0) {
return result;
}
if (const int result = require(step.boundaryConditionIndices()[1] == 2); result != 0) {
return result;
}
if (const int result = require(step.loadIndices().size() == 1); result != 0) {
return result;
}
if (const int result = require(step.loadIndices()[0] == 1); result != 0) {
return result;
}
return 0;
}