From fdeac602f406eed31b3026d3d667d97219c277de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EA=B2=BD=EC=A2=85?= Date: Mon, 8 Jun 2026 16:40:04 +0900 Subject: [PATCH] feat: add domain model foundation --- CMakeLists.txt | 33 + docs/PROGRESS.md | 12 +- docs/WORKNOTE.md | 3 + .../domain-model-foundation-build-test.md | 74 ++ ...in-model-foundation-implementation-plan.md | 190 +++++ include/fesa/core/BoundaryCondition.hpp | 21 + include/fesa/core/Domain.hpp | 77 ++ include/fesa/core/ElementDefinition.hpp | 33 + include/fesa/core/LoadDefinition.hpp | 21 + include/fesa/core/MaterialDefinition.hpp | 21 + include/fesa/core/ModelTypes.hpp | 26 + include/fesa/core/Node.hpp | 24 + include/fesa/core/PropertyDefinition.hpp | 21 + include/fesa/core/StepDefinition.hpp | 31 + phases/domain-model-foundation/index.json | 60 ++ phases/domain-model-foundation/step0.md | 77 ++ phases/domain-model-foundation/step1.md | 70 ++ phases/domain-model-foundation/step2.md | 76 ++ phases/domain-model-foundation/step3.md | 63 ++ phases/domain-model-foundation/step4.md | 65 ++ phases/domain-model-foundation/step5.md | 66 ++ phases/domain-model-foundation/step6.md | 66 ++ phases/domain-model-foundation/step7.md | 61 ++ phases/domain-model-foundation/step8.md | 63 ++ phases/index.json | 8 + scripts/test_validate_workspace.py | 57 +- scripts/validate_workspace.py | 34 +- src/core/Domain.cpp | 381 ++++++++++ tests/core/boundary_condition_test.cpp | 25 + tests/core/domain_bootstrap_test.cpp | 40 ++ tests/core/domain_storage_test.cpp | 655 ++++++++++++++++++ tests/core/element_definition_test.cpp | 41 ++ tests/core/load_definition_test.cpp | 25 + tests/core/material_definition_test.cpp | 25 + tests/core/model_types_test.cpp | 46 ++ tests/core/node_test.cpp | 37 + tests/core/property_definition_test.cpp | 25 + tests/core/step_definition_test.cpp | 37 + 38 files changed, 2685 insertions(+), 5 deletions(-) create mode 100644 CMakeLists.txt create mode 100644 docs/build-test-reports/domain-model-foundation-build-test.md create mode 100644 docs/implementation-plans/domain-model-foundation-implementation-plan.md create mode 100644 include/fesa/core/BoundaryCondition.hpp create mode 100644 include/fesa/core/Domain.hpp create mode 100644 include/fesa/core/ElementDefinition.hpp create mode 100644 include/fesa/core/LoadDefinition.hpp create mode 100644 include/fesa/core/MaterialDefinition.hpp create mode 100644 include/fesa/core/ModelTypes.hpp create mode 100644 include/fesa/core/Node.hpp create mode 100644 include/fesa/core/PropertyDefinition.hpp create mode 100644 include/fesa/core/StepDefinition.hpp create mode 100644 phases/domain-model-foundation/index.json create mode 100644 phases/domain-model-foundation/step0.md create mode 100644 phases/domain-model-foundation/step1.md create mode 100644 phases/domain-model-foundation/step2.md create mode 100644 phases/domain-model-foundation/step3.md create mode 100644 phases/domain-model-foundation/step4.md create mode 100644 phases/domain-model-foundation/step5.md create mode 100644 phases/domain-model-foundation/step6.md create mode 100644 phases/domain-model-foundation/step7.md create mode 100644 phases/domain-model-foundation/step8.md create mode 100644 phases/index.json create mode 100644 src/core/Domain.cpp create mode 100644 tests/core/boundary_condition_test.cpp create mode 100644 tests/core/domain_bootstrap_test.cpp create mode 100644 tests/core/domain_storage_test.cpp create mode 100644 tests/core/element_definition_test.cpp create mode 100644 tests/core/load_definition_test.cpp create mode 100644 tests/core/material_definition_test.cpp create mode 100644 tests/core/model_types_test.cpp create mode 100644 tests/core/node_test.cpp create mode 100644 tests/core/property_definition_test.cpp create mode 100644 tests/core/step_definition_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e937a20 --- /dev/null +++ b/CMakeLists.txt @@ -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") diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index f43e462..969162d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -8,6 +8,7 @@ - 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/`. - 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 - 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/` 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. - 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 -- 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 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`. ## 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 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. diff --git a/docs/WORKNOTE.md b/docs/WORKNOTE.md index 362cd16..02f92f3 100644 --- a/docs/WORKNOTE.md +++ b/docs/WORKNOTE.md @@ -13,6 +13,9 @@ During planning, standard checks for `HDF5_ROOT`, `HDF5_DIR`, `h5dump`, and obvi ### 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. +### 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 `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. diff --git a/docs/build-test-reports/domain-model-foundation-build-test.md b/docs/build-test-reports/domain-model-foundation-build-test.md new file mode 100644 index 0000000..9ca608e --- /dev/null +++ b/docs/build-test-reports/domain-model-foundation-build-test.md @@ -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. diff --git a/docs/implementation-plans/domain-model-foundation-implementation-plan.md b/docs/implementation-plans/domain-model-foundation-implementation-plan.md new file mode 100644 index 0000000..6341894 --- /dev/null +++ b/docs/implementation-plans/domain-model-foundation-implementation-plan.md @@ -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. diff --git a/include/fesa/core/BoundaryCondition.hpp b/include/fesa/core/BoundaryCondition.hpp new file mode 100644 index 0000000..7ab0ed9 --- /dev/null +++ b/include/fesa/core/BoundaryCondition.hpp @@ -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 diff --git a/include/fesa/core/Domain.hpp b/include/fesa/core/Domain.hpp new file mode 100644 index 0000000..4b7eafe --- /dev/null +++ b/include/fesa/core/Domain.hpp @@ -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 +#include +#include +#include + +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 node_ids); + void addElementSet(std::string name, std::vector 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* findNodeSet(const std::string& name) const noexcept; + const std::vector& nodeSet(const std::string& name) const; + std::size_t nodeSetCount() const noexcept; + + const std::vector* findElementSet(const std::string& name) const noexcept; + const std::vector& 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 nodes_; + std::unordered_map elements_; + std::unordered_map materials_; + std::unordered_map shell_properties_; + std::unordered_map> node_sets_; + std::unordered_map> element_sets_; + std::vector boundary_conditions_; + std::vector nodal_loads_; + std::unordered_map steps_; +}; + +} // namespace fesa::core diff --git a/include/fesa/core/ElementDefinition.hpp b/include/fesa/core/ElementDefinition.hpp new file mode 100644 index 0000000..c2cacfb --- /dev/null +++ b/include/fesa/core/ElementDefinition.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" + +#include + +namespace fesa::core { + +enum class ElementType { + Mitc4 +}; + +class ElementDefinition { +public: + ElementDefinition( + ElementId id, + ElementType type, + std::array connectivity, + PropertyId property_id); + + ElementId id() const noexcept; + ElementType type() const noexcept; + const std::array& connectivity() const noexcept; + PropertyId propertyId() const noexcept; + +private: + ElementId id_; + ElementType type_; + std::array connectivity_; + PropertyId property_id_; +}; + +} // namespace fesa::core diff --git a/include/fesa/core/LoadDefinition.hpp b/include/fesa/core/LoadDefinition.hpp new file mode 100644 index 0000000..bbb29ed --- /dev/null +++ b/include/fesa/core/LoadDefinition.hpp @@ -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 diff --git a/include/fesa/core/MaterialDefinition.hpp b/include/fesa/core/MaterialDefinition.hpp new file mode 100644 index 0000000..a33cbf8 --- /dev/null +++ b/include/fesa/core/MaterialDefinition.hpp @@ -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 diff --git a/include/fesa/core/ModelTypes.hpp b/include/fesa/core/ModelTypes.hpp new file mode 100644 index 0000000..18d4ffb --- /dev/null +++ b/include/fesa/core/ModelTypes.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +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 diff --git a/include/fesa/core/Node.hpp b/include/fesa/core/Node.hpp new file mode 100644 index 0000000..e77398e --- /dev/null +++ b/include/fesa/core/Node.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" + +#include + +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& coordinates() const noexcept; + +private: + NodeId id_; + std::array coordinates_; +}; + +} // namespace fesa::core diff --git a/include/fesa/core/PropertyDefinition.hpp b/include/fesa/core/PropertyDefinition.hpp new file mode 100644 index 0000000..03bbe4f --- /dev/null +++ b/include/fesa/core/PropertyDefinition.hpp @@ -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 diff --git a/include/fesa/core/StepDefinition.hpp b/include/fesa/core/StepDefinition.hpp new file mode 100644 index 0000000..9e24a20 --- /dev/null +++ b/include/fesa/core/StepDefinition.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" + +#include +#include +#include + +namespace fesa::core { + +class LinearStaticStepDefinition { +public: + LinearStaticStepDefinition( + StepId id, + std::string name, + std::vector boundary_condition_indices, + std::vector load_indices); + + StepId id() const noexcept; + const std::string& name() const noexcept; + const std::vector& boundaryConditionIndices() const noexcept; + const std::vector& loadIndices() const noexcept; + +private: + StepId id_; + std::string name_; + std::vector boundary_condition_indices_; + std::vector load_indices_; +}; + +} // namespace fesa::core diff --git a/phases/domain-model-foundation/index.json b/phases/domain-model-foundation/index.json new file mode 100644 index 0000000..ceba6cf --- /dev/null +++ b/phases/domain-model-foundation/index.json @@ -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." + } + ] +} diff --git a/phases/domain-model-foundation/step0.md b/phases/domain-model-foundation/step0.md new file mode 100644 index 0000000..847f3bf --- /dev/null +++ b/phases/domain-model-foundation/step0.md @@ -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. diff --git a/phases/domain-model-foundation/step1.md b/phases/domain-model-foundation/step1.md new file mode 100644 index 0000000..501876c --- /dev/null +++ b/phases/domain-model-foundation/step1.md @@ -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. diff --git a/phases/domain-model-foundation/step2.md b/phases/domain-model-foundation/step2.md new file mode 100644 index 0000000..3282804 --- /dev/null +++ b/phases/domain-model-foundation/step2.md @@ -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. diff --git a/phases/domain-model-foundation/step3.md b/phases/domain-model-foundation/step3.md new file mode 100644 index 0000000..b09cc40 --- /dev/null +++ b/phases/domain-model-foundation/step3.md @@ -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. diff --git a/phases/domain-model-foundation/step4.md b/phases/domain-model-foundation/step4.md new file mode 100644 index 0000000..3465aae --- /dev/null +++ b/phases/domain-model-foundation/step4.md @@ -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. diff --git a/phases/domain-model-foundation/step5.md b/phases/domain-model-foundation/step5.md new file mode 100644 index 0000000..e440135 --- /dev/null +++ b/phases/domain-model-foundation/step5.md @@ -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. diff --git a/phases/domain-model-foundation/step6.md b/phases/domain-model-foundation/step6.md new file mode 100644 index 0000000..bc146f8 --- /dev/null +++ b/phases/domain-model-foundation/step6.md @@ -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. diff --git a/phases/domain-model-foundation/step7.md b/phases/domain-model-foundation/step7.md new file mode 100644 index 0000000..4b6f435 --- /dev/null +++ b/phases/domain-model-foundation/step7.md @@ -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. diff --git a/phases/domain-model-foundation/step8.md b/phases/domain-model-foundation/step8.md new file mode 100644 index 0000000..e1fdddc --- /dev/null +++ b/phases/domain-model-foundation/step8.md @@ -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. diff --git a/phases/index.json b/phases/index.json new file mode 100644 index 0000000..ad8f5c8 --- /dev/null +++ b/phases/index.json @@ -0,0 +1,8 @@ +{ + "phases": [ + { + "dir": "domain-model-foundation", + "status": "completed" + } + ] +} diff --git a/scripts/test_validate_workspace.py b/scripts/test_validate_workspace.py index 71ba75a..c2da5c6 100644 --- a/scripts/test_validate_workspace.py +++ b/scripts/test_validate_workspace.py @@ -87,7 +87,62 @@ class ValidateWorkspaceTests(unittest.TestCase): with patch.object(validate_workspace.shutil, "which", return_value=None): 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__": diff --git a/scripts/validate_workspace.py b/scripts/validate_workspace.py index 45b4530..2b155c1 100644 --- a/scripts/validate_workspace.py +++ b/scripts/validate_workspace.py @@ -88,7 +88,27 @@ def discover_commands(root: Path) -> list[str]: 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: + command = resolve_validation_command(command) return subprocess.run( command, 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]: 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 cmake_exe = COMMON_CMAKE_BIN / "cmake.exe" if not cmake_exe.exists(): return env - current_path = env.get("PATH", "") paths = [part for part in current_path.split(os.pathsep) if part] common_bin_text = str(COMMON_CMAKE_BIN) 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 diff --git a/src/core/Domain.cpp b/src/core/Domain.cpp new file mode 100644 index 0000000..b9789e3 --- /dev/null +++ b/src/core/Domain.cpp @@ -0,0 +1,381 @@ +#include "fesa/core/Domain.hpp" + +#include +#include + +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 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& 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 boundary_condition_indices, + std::vector 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& LinearStaticStepDefinition::boundaryConditionIndices() const noexcept { + return boundary_condition_indices_; +} + +const std::vector& 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& 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 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 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* 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& Domain::nodeSet(const std::string& name) const { + const std::vector* 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* 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& Domain::elementSet(const std::string& name) const { + const std::vector* 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 diff --git a/tests/core/boundary_condition_test.cpp b/tests/core/boundary_condition_test.cpp new file mode 100644 index 0000000..f1d19cb --- /dev/null +++ b/tests/core/boundary_condition_test.cpp @@ -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; +} diff --git a/tests/core/domain_bootstrap_test.cpp b/tests/core/domain_bootstrap_test.cpp new file mode 100644 index 0000000..9e2aac7 --- /dev/null +++ b/tests/core/domain_bootstrap_test.cpp @@ -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; +} diff --git a/tests/core/domain_storage_test.cpp b/tests/core/domain_storage_test.cpp new file mode 100644 index 0000000..18a0d57 --- /dev/null +++ b/tests/core/domain_storage_test.cpp @@ -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 +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +template +int require_throws(Function&& function) { + try { + function(); + } catch (const Exception&) { + return 0; + } catch (...) { + return 1; + } + return 1; +} + +int 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([&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([&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([&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([&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([&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([&domain]() { + domain.addMaterial(fesa::core::LinearElasticMaterialDefinition{700, 100.0, 0.25}); + }); + result != 0) { + return result; + } + + return require_throws([&domain]() { + domain.addShellProperty(fesa::core::ShellPropertyDefinition{500, 700, 0.02}); + }); +} + +int shell_property_referencing_missing_material_throws() { + fesa::core::Domain domain; + + return require_throws([&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([&domain]() { + domain.addNodeSet("left-edge", {1}); + }); + result != 0) { + return result; + } + return require_throws([&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([&domain]() { + domain.addNodeSet("bad-nodes", {1, 99}); + }); + result != 0) { + return result; + } + return require_throws([&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([&domain]() { + (void)domain.addBoundaryCondition( + fesa::core::BoundaryCondition{99, fesa::core::Dof::U1, 0.0}); + }); + result != 0) { + return result; + } + + return require_throws([&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([&domain, bc, load]() { + domain.addStep(fesa::core::LinearStaticStepDefinition{1, "duplicate", {bc}, {load}}); + }); + result != 0) { + return result; + } + if (const int result = require_throws([&domain, load]() { + domain.addStep(fesa::core::LinearStaticStepDefinition{2, "bad-bc", {99}, {load}}); + }); + result != 0) { + return result; + } + return require_throws([&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::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same&>::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same&>::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same::value)); + result != 0) { + return result; + } + if (const int result = require((std::is_same::value)); + result != 0) { + return result; + } + return require((std::is_same::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([&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([&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([&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([&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([&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; +} diff --git a/tests/core/element_definition_test.cpp b/tests/core/element_definition_test.cpp new file mode 100644 index 0000000..76c0999 --- /dev/null +++ b/tests/core/element_definition_test.cpp @@ -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; +} diff --git a/tests/core/load_definition_test.cpp b/tests/core/load_definition_test.cpp new file mode 100644 index 0000000..ccb465b --- /dev/null +++ b/tests/core/load_definition_test.cpp @@ -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; +} diff --git a/tests/core/material_definition_test.cpp b/tests/core/material_definition_test.cpp new file mode 100644 index 0000000..957f18f --- /dev/null +++ b/tests/core/material_definition_test.cpp @@ -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; +} diff --git a/tests/core/model_types_test.cpp b/tests/core/model_types_test.cpp new file mode 100644 index 0000000..1900f13 --- /dev/null +++ b/tests/core/model_types_test.cpp @@ -0,0 +1,46 @@ +#include "fesa/core/ModelTypes.hpp" + +#include +#include + +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::value); result != 0) { + return result; + } + if (const int result = require(kDofPerNode == 6); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::U1) == 0); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::U2) == 1); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::U3) == 2); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::UR1) == 3); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::UR2) == 4); result != 0) { + return result; + } + if (const int result = require(static_cast(Dof::UR3) == 5); result != 0) { + return result; + } + + return 0; +} diff --git a/tests/core/node_test.cpp b/tests/core/node_test.cpp new file mode 100644 index 0000000..750e5d3 --- /dev/null +++ b/tests/core/node_test.cpp @@ -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; +} diff --git a/tests/core/property_definition_test.cpp b/tests/core/property_definition_test.cpp new file mode 100644 index 0000000..7ce61e5 --- /dev/null +++ b/tests/core/property_definition_test.cpp @@ -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; +} diff --git a/tests/core/step_definition_test.cpp b/tests/core/step_definition_test.cpp new file mode 100644 index 0000000..6c3ff61 --- /dev/null +++ b/tests/core/step_definition_test.cpp @@ -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; +}