diff --git a/CMakeLists.txt b/CMakeLists.txt index e937a20..f057e31 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,7 +7,12 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) add_library(fesa_core STATIC + src/boundary/SinglePointConstraint.cpp src/core/Domain.cpp + src/element/Mitc4Element.cpp + src/load/NodalLoad.cpp + src/material/LinearElasticMaterial.cpp + src/property/ShellProperty.cpp ) target_include_directories(fesa_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include @@ -18,6 +23,7 @@ enable_testing() add_executable(fesa_domain_tests tests/core/boundary_condition_test.cpp tests/core/domain_bootstrap_test.cpp + tests/core/domain_model_object_test.cpp tests/core/domain_storage_test.cpp tests/core/element_definition_test.cpp tests/core/load_definition_test.cpp @@ -31,3 +37,17 @@ 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") + +add_executable(fesa_model_object_tests + tests/boundary/boundary_base_test.cpp + tests/element/element_base_test.cpp + tests/element/mitc4_element_model_test.cpp + tests/load/load_base_test.cpp + tests/material/material_base_test.cpp + tests/model_object_main.cpp + tests/property/shell_property_test.cpp +) +target_link_libraries(fesa_model_object_tests PRIVATE fesa_core) + +add_test(NAME model-object.base COMMAND fesa_model_object_tests) +set_tests_properties(model-object.base PROPERTIES LABELS "model-object;core") diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 969162d..69bfb0a 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -9,6 +9,7 @@ - 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. +- Analysis model object phase exists under `phases/analysis-model-objects/` and adds identity-only model objects for Domain ownership. ## Completed - Defined the nine-step solver development workflow. @@ -29,6 +30,10 @@ - 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. +- Created `docs/implementation-plans/analysis-model-objects-implementation-plan.md`. +- Added identity-only model object base classes and concrete skeletons for `Element`/`Mitc4Element`, `Material`/`LinearElasticMaterial`, `ShellProperty`, `Load`/`NodalLoad`, and `BoundaryCondition`/`SinglePointConstraint`. +- Extended `Domain` with RAII `std::unique_ptr` ownership APIs for polymorphic element, material, load, and boundary model objects while preserving existing `*Definition` storage APIs. +- Added `fesa_model_object_tests` CTest target and model-object tests under `tests/element/`, `tests/material/`, `tests/property/`, `tests/load/`, and `tests/boundary/`. ## In Progress - Ready for the next upstream MITC4 stage: new solver feature requirements analysis for `mitc4-linear-static-shell`. @@ -42,6 +47,10 @@ 6. Create `docs/implementation-plans/mitc4-linear-static-shell-implementation-plan.md`. ## Last Validation +- 2026-06-09: After analysis model object implementation, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully. +- 2026-06-09: After analysis model object implementation, `python scripts/validate_workspace.py` configured CMake with Visual Studio 17 2022 x64, built Debug targets, ran CTest, and passed. +- 2026-06-09: After analysis model object implementation, `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"` passed. 2 CTest executables ran successfully. +- 2026-06-09: After analysis model object implementation, `git diff --check` passed. - 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. diff --git a/docs/build-test-reports/analysis-model-objects-build-test.md b/docs/build-test-reports/analysis-model-objects-build-test.md new file mode 100644 index 0000000..9914a5c --- /dev/null +++ b/docs/build-test-reports/analysis-model-objects-build-test.md @@ -0,0 +1,73 @@ +# Analysis Model Objects Build/Test Report + +## Metadata +- feature_id: analysis-model-objects +- source_implementation_report: N/A +- source_implementation_plan: docs/implementation-plans/analysis-model-objects-implementation-plan.md +- status: pass-for-next-implementation-stage +- owner_agent: build-test-executor-agent +- date: 2026-06-09 + +## Execution Environment +- os: Windows +- generator: Visual Studio 17 2022 +- platform: x64 +- config: Debug +- build_dir: build/msvc-debug +- active_override_env_vars: none +- command_discovery_path: default CMake/MSVC x64 Debug through `python scripts/validate_workspace.py` + +## Command Log Summary + +| order | command | exit_code | duration | stdout_stderr_tail | +| --- | --- | --- | --- | --- | +| 1 | `python -m unittest discover -s scripts -p "test_*.py"` | 0 | <1s | 89 tests passed | +| 2 | `python scripts/validate_workspace.py` | 0 | ~4s | CMake configure, Debug build, and CTest passed | +| 3 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"` | 0 | <1s | 2 CTest executables passed | +| 4 | `git diff --check` | 0 | <1s | no whitespace errors; line-ending normalization warnings may appear | + +## Validation Results + +| validation_stage | result | evidence | +| --- | --- | --- | +| harness self-test | pass | Python unittest discovery passed 89 tests | +| configure | pass | CMake generated `build/msvc-debug` with Visual Studio 17 2022 x64 | +| build | pass | `fesa_core.lib`, `fesa_domain_tests.exe`, and `fesa_model_object_tests.exe` built | +| CTest | pass | `domain.bootstrap` and `model-object.base` passed | +| feature-specific tests | pass | model-object and domain CTest filters passed | + +## 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 | +| --- | --- | --- | +| Parser/Factory Implementation Agent | New model objects can be populated from future parser/factory mappings | model object headers and compatibility note | +| AnalysisModel Implementation Agent | Domain now exposes identity-only model objects through const base APIs | Domain object ownership APIs | +| 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 model object classes and Domain ownership APIs only. It did not run reference solvers, generate reference artifacts, change tolerances, or claim numerical MITC4 correctness. + +## Open Issues + +- Parser/factory mapping from core `*Definition` DTOs to model objects remains deferred. +- MITC4 stiffness, material constitutive behavior, shell section stiffness, assembly, solver, and HDF5 output remain future phases. diff --git a/docs/implementation-plans/analysis-model-objects-implementation-plan.md b/docs/implementation-plans/analysis-model-objects-implementation-plan.md new file mode 100644 index 0000000..a61cc2d --- /dev/null +++ b/docs/implementation-plans/analysis-model-objects-implementation-plan.md @@ -0,0 +1,179 @@ +# Analysis Model Objects Implementation Plan + +## Metadata +- feature_id: analysis-model-objects +- source_requirement: AGENTS.md; docs/ARCHITECTURE.md; docs/ADR.md +- source_research: N/A for this architecture/model-object slice +- source_formulation: N/A for this architecture/model-object slice +- source_numerical_review: N/A for this architecture/model-object slice +- source_io_definition: docs/ARCHITECTURE.md Domain and factory/registry ownership rules +- source_reference_models: N/A for this architecture/model-object slice +- status: ready-for-implementation +- owner_agent: implementation-planning-agent +- date: 2026-06-09 + +## Readiness Check + +| input | required_status | observed_status | decision | +| --- | --- | --- | --- | +| architecture | model object boundaries documented | documented in docs/ARCHITECTURE.md and ADR-004/010/011 | proceed | +| domain foundation | base Domain storage available | implemented in `domain-model-foundation` | proceed | +| formulation | not required for identity-only model object skeletons | N/A | proceed | +| numerical_review | not required for identity-only model object skeletons | N/A | proceed | +| io_definition | parser DTO compatibility sufficient | existing `*Definition` objects remain input DTOs | proceed | +| reference_models | not required because this slice produces no solver result | N/A | proceed | + +## Implementation Scope + +- included_behavior: C++17 model object classes for nodes, elements, materials, shell properties, loads, and boundary conditions. +- included_behavior: abstract base classes for element, material, load, and boundary condition model objects. +- included_behavior: MITC4 element model skeleton with id, connectivity, property id, 4-node count, and 24-DOF count. +- included_behavior: Domain RAII ownership for polymorphic model objects through `std::unique_ptr`. +- included_behavior: compatibility with existing `*Definition` value objects and tests. +- excluded_behavior: MITC4 stiffness, Jacobian, MITC shear interpolation, constitutive matrices, global force assembly, boundary-condition elimination, equation numbering, solver vectors, reaction recovery, HDF5 output, parser/factory implementation, and reference comparison. +- non_goals: numerical correctness claims, release readiness, reference-solver execution, and reference artifact generation. + +## Model Object Contract + +`Domain` remains the owner of parsed model definition. This phase adds runtime model objects that can later be used by `AnalysisModel`, factories, and element/material implementations without storing analysis state in `Domain`. + +Model object responsibilities: + +- `Node`: id and reference coordinates only; fixed six-DOF count from project convention. +- `Element`: identity, element type, connectivity, property reference, and node count only. +- `Mitc4Element`: MITC4 model skeleton with four node ids and 24 element DOFs only. +- `Material`: material identity only. +- `LinearElasticMaterial`: id, Young's modulus, and Poisson ratio only. +- `ShellProperty`: property id, material id, and thickness only. +- `Load`: load kind only. +- `NodalLoad`: node id, DOF, and scalar value only. +- `BoundaryCondition`: boundary kind only. +- `SinglePointConstraint`: node id, DOF, and prescribed scalar value only. + +Excluded from all model objects: + +- equation ids +- sparse matrix data +- displacement, residual, reaction, velocity, acceleration, or temperature vectors +- current time, increment, or iteration counters +- element integration point state +- MKL, TBB, HDF5, or reference-comparison handles + +Existing `*Definition` value objects remain parser/factory DTOs for now. This phase must not delete them. Later parser/factory work may either map DTOs into model objects or retire DTOs with an explicit migration plan. + +Compatibility decision: + +- Existing `ElementDefinition`, `LinearElasticMaterialDefinition`, `ShellPropertyDefinition`, `BoundaryCondition`, `NodalLoadDefinition`, and `LinearStaticStepDefinition` classes remain under `include/fesa/core/` as value-style input DTOs. +- New runtime model objects live under ownership namespaces: `fesa::element`, `fesa::material`, `fesa::property`, `fesa::load`, and `fesa::boundary`. +- `Domain` supports both existing DTO storage APIs and new polymorphic model-object ownership APIs during the transition. +- Parser/factory work must make an explicit choice to map DTOs to model objects or replace DTOs in a separate phase. This phase does not decide parser behavior. + +## Work Breakdown + +| task_id | order | purpose | upstream_trace | depends_on | expected_test_first | +| --- | --- | --- | --- | --- | --- | +| AMO-001 | 1 | refine `Node` model class contract | ADR-010; docs/ARCHITECTURE.md Node | none | AMO-TEST-001 | +| AMO-002 | 2 | add abstract `Element` base interface | ADR-004; docs/ARCHITECTURE.md Element | AMO-001 | AMO-TEST-002 | +| AMO-003 | 3 | add MITC4 element model skeleton | AGENTS.md MITC4 scope | AMO-002 | AMO-TEST-003 | +| AMO-004 | 4 | add material base and linear elastic material object | docs/ARCHITECTURE.md Material | AMO-002 | AMO-TEST-004 | +| AMO-005 | 5 | add shell property model object | docs/ARCHITECTURE.md Property | AMO-004 | AMO-TEST-005 | +| AMO-006 | 6 | add load base and nodal load object | docs/ARCHITECTURE.md loads | AMO-002 | AMO-TEST-006 | +| AMO-007 | 7 | add boundary base and single-point constraint object | ADR-012 | AMO-002 | AMO-TEST-007 | +| AMO-008 | 8 | extend Domain with polymorphic object ownership | ADR-010; ADR-011 | AMO-003 to AMO-007 | AMO-TEST-008 | +| AMO-009 | 9 | preserve `*Definition` compatibility | domain-model-foundation plan | AMO-008 | AMO-TEST-009 | +| AMO-010 | 10 | record build/test evidence and handoff | docs/build-test-reports/README.md | AMO-009 | AMO-TEST-010 | + +## TDD Test Plan + +| test_id | order | test_type | red_condition | green_condition | linked_task | command | +| --- | --- | --- | --- | --- | --- | --- | +| AMO-TEST-001 | 1 | unit | `Node::dofCount()` missing | Node model tests pass | AMO-001 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` | +| AMO-TEST-002 | 2 | unit | `fesa/element/Element.hpp` missing | Element base polymorphism tests pass | AMO-002 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-003 | 3 | unit | `Mitc4Element` missing | MITC4 model skeleton tests pass | AMO-003 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-004 | 4 | unit | material model headers missing | material base and linear elastic tests pass | AMO-004 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-005 | 5 | unit | shell property model missing | shell property tests pass | AMO-005 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-006 | 6 | unit | load model headers missing | load base and nodal load tests pass | AMO-006 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-007 | 7 | unit | boundary model headers missing | boundary base and SPC tests pass | AMO-007 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object` | +| AMO-TEST-008 | 8 | unit | Domain polymorphic APIs missing | Domain owns and retrieves model objects by base interface | AMO-008 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"` | +| AMO-TEST-009 | 9 | regression | existing definition tests fail | existing Domain definition tests remain passing | AMO-009 | `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain` | +| AMO-TEST-010 | 10 | validation | report evidence missing | validation report records passing commands or classified failure | AMO-010 | `python scripts/validate_workspace.py` | + +## CMake/CTest Plan + +- target_candidates: `fesa_core`, `fesa_domain_tests`, `fesa_model_object_tests` +- labels: `domain`, `core`, `model-object`, `element`, `material`, `property`, `load`, `boundary` +- msvc_config: Debug +- expected_feature_command: `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"` +- workspace_validation: `python scripts/validate_workspace.py` + +## Candidate Files and Ownership + +| file_candidate | purpose | owner_boundary | notes | +| --- | --- | --- | --- | +| `include/fesa/core/Node.hpp` | existing node model object | core model definition | no solver state | +| `include/fesa/element/Element.hpp` | abstract element model base | element model identity | no stiffness API | +| `include/fesa/element/Mitc4Element.hpp` | MITC4 element model skeleton | element model identity | no formulation implementation | +| `src/element/Mitc4Element.cpp` | MITC4 model accessors | element model identity | no numerical behavior | +| `include/fesa/material/Material.hpp` | abstract material model base | material identity | no constitutive matrix | +| `include/fesa/material/LinearElasticMaterial.hpp` | linear elastic material model | material input values | no stress update | +| `src/material/LinearElasticMaterial.cpp` | linear elastic material accessors | material input values | no numerical behavior | +| `include/fesa/property/ShellProperty.hpp` | shell property model | property input values | no section stiffness | +| `src/property/ShellProperty.cpp` | shell property validation/accessors | property input values | thickness validation only | +| `include/fesa/load/Load.hpp` | abstract load model base | load identity | no assembly | +| `include/fesa/load/NodalLoad.hpp` | nodal load model | load input values | no global vector behavior | +| `src/load/NodalLoad.cpp` | nodal load accessors | load input values | no assembly | +| `include/fesa/boundary/BoundaryCondition.hpp` | abstract boundary model base | boundary identity | no matrix application | +| `include/fesa/boundary/SinglePointConstraint.hpp` | SPC boundary model | boundary input values | no reaction recovery | +| `src/boundary/SinglePointConstraint.cpp` | SPC accessors | boundary input values | no matrix application | +| `include/fesa/core/Domain.hpp` | Domain ownership extension | core ownership | preserve existing APIs | +| `src/core/Domain.cpp` | Domain ownership implementation | core ownership | no solver state | +| `tests/**` | TDD coverage | tests | write before production changes | + +## Acceptance Traceability Matrix + +| requirement_id | task_id | test_id | reference_model_id | acceptance_criterion | status | +| --- | --- | --- | --- | --- | --- | +| AMO-REQ-001 Node is model data only | AMO-001 | AMO-TEST-001 | N/A | Node tests pass and no solver state added | draft | +| AMO-REQ-002 Element base is identity-only | AMO-002 | AMO-TEST-002 | N/A | Element base tests pass | draft | +| AMO-REQ-003 MITC4 model skeleton has 4 nodes and 24 DOFs | AMO-003 | AMO-TEST-003 | N/A | MITC4 model tests pass | draft | +| AMO-REQ-004 Material model stores input values only | AMO-004 | AMO-TEST-004 | N/A | material tests pass | draft | +| AMO-REQ-005 Shell property validates thickness only | AMO-005 | AMO-TEST-005 | N/A | shell property tests pass | draft | +| AMO-REQ-006 Load and boundary models do not assemble/apply | AMO-006; AMO-007 | AMO-TEST-006; AMO-TEST-007 | N/A | load/boundary tests pass and APIs remain value accessors | draft | +| AMO-REQ-007 Domain owns polymorphic model objects via RAII | AMO-008 | AMO-TEST-008 | N/A | Domain ownership tests pass | draft | +| AMO-REQ-008 Existing definition APIs remain compatible | AMO-009 | AMO-TEST-009 | N/A | existing domain tests pass | 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|model-object" +``` + +## Risks and Downstream Handoff + +### Implementation Agent + +- Keep model objects identity-only until formulation and solver stages define numerical APIs. + - Preserve existing `*Definition` tests. +- Prefer RAII ownership through `std::unique_ptr` and const retrieval. + +### Build/Test Executor Agent + +- Use Visual Studio 17 2022, x64, Debug, and `build/msvc-debug`. +- Use `python scripts/validate_workspace.py` as canonical validation. + +### Correction Agent + +- Implementation-owned failures are expected to be compile or unit-test failures in model object headers, model object sources, CMake registration, or Domain ownership methods. +- Upstream-contract failures include requests to add numerical MITC4 behavior or solver state in this phase. + +### Reference Verification Agent + +- No reference verification is required for this phase. +- This phase produces no HDF5 result and consumes no reference artifacts. + +## Open Issues + +- Parser/factory mapping from `*Definition` DTOs to model objects remains deferred. +- MITC4 formulation, material constitutive behavior, shell section stiffness, assembly, solver, and HDF5 output remain separate future phases. diff --git a/include/fesa/boundary/BoundaryCondition.hpp b/include/fesa/boundary/BoundaryCondition.hpp new file mode 100644 index 0000000..bab4b95 --- /dev/null +++ b/include/fesa/boundary/BoundaryCondition.hpp @@ -0,0 +1,16 @@ +#pragma once + +namespace fesa::boundary { + +enum class BoundaryConditionKind { + SinglePointConstraint +}; + +class BoundaryCondition { +public: + virtual ~BoundaryCondition() = default; + + virtual BoundaryConditionKind kind() const noexcept = 0; +}; + +} // namespace fesa::boundary diff --git a/include/fesa/boundary/SinglePointConstraint.hpp b/include/fesa/boundary/SinglePointConstraint.hpp new file mode 100644 index 0000000..ee9b7b1 --- /dev/null +++ b/include/fesa/boundary/SinglePointConstraint.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "fesa/boundary/BoundaryCondition.hpp" +#include "fesa/core/ModelTypes.hpp" + +namespace fesa::boundary { + +using fesa::core::Dof; +using fesa::core::NodeId; + +class SinglePointConstraint final : public BoundaryCondition { +public: + SinglePointConstraint(NodeId node_id, Dof dof, double value); + + BoundaryConditionKind kind() const noexcept override; + NodeId nodeId() const noexcept; + Dof dof() const noexcept; + double value() const noexcept; + +private: + NodeId node_id_; + Dof dof_; + double value_; +}; + +} // namespace fesa::boundary diff --git a/include/fesa/core/Domain.hpp b/include/fesa/core/Domain.hpp index 4b7eafe..f9ebaf6 100644 --- a/include/fesa/core/Domain.hpp +++ b/include/fesa/core/Domain.hpp @@ -1,5 +1,6 @@ #pragma once +#include "fesa/boundary/BoundaryCondition.hpp" #include "fesa/core/BoundaryCondition.hpp" #include "fesa/core/ElementDefinition.hpp" #include "fesa/core/LoadDefinition.hpp" @@ -8,8 +9,12 @@ #include "fesa/core/Node.hpp" #include "fesa/core/PropertyDefinition.hpp" #include "fesa/core/StepDefinition.hpp" +#include "fesa/element/Element.hpp" +#include "fesa/load/Load.hpp" +#include "fesa/material/Material.hpp" #include +#include #include #include #include @@ -27,6 +32,10 @@ public: std::size_t addBoundaryCondition(BoundaryCondition condition); std::size_t addNodalLoad(NodalLoadDefinition load); void addStep(LinearStaticStepDefinition step); + void addElementObject(std::unique_ptr element); + void addMaterialObject(std::unique_ptr material); + std::size_t addLoadObject(std::unique_ptr load); + std::size_t addBoundaryObject(std::unique_ptr boundary); const Node* findNode(NodeId id) const noexcept; const Node& node(NodeId id) const; @@ -62,6 +71,22 @@ public: const LinearStaticStepDefinition& step(StepId id) const; std::size_t stepCount() const noexcept; + const fesa::element::Element* findElementObject(ElementId id) const noexcept; + const fesa::element::Element& elementObject(ElementId id) const; + std::size_t elementObjectCount() const noexcept; + + const fesa::material::Material* findMaterialObject(MaterialId id) const noexcept; + const fesa::material::Material& materialObject(MaterialId id) const; + std::size_t materialObjectCount() const noexcept; + + const fesa::load::Load* findLoadObject(std::size_t index) const noexcept; + const fesa::load::Load& loadObject(std::size_t index) const; + std::size_t loadObjectCount() const noexcept; + + const fesa::boundary::BoundaryCondition* findBoundaryObject(std::size_t index) const noexcept; + const fesa::boundary::BoundaryCondition& boundaryObject(std::size_t index) const; + std::size_t boundaryObjectCount() const noexcept; + private: std::unordered_map nodes_; std::unordered_map elements_; @@ -72,6 +97,10 @@ private: std::vector boundary_conditions_; std::vector nodal_loads_; std::unordered_map steps_; + std::unordered_map> element_objects_; + std::unordered_map> material_objects_; + std::vector> load_objects_; + std::vector> boundary_objects_; }; } // namespace fesa::core diff --git a/include/fesa/core/Node.hpp b/include/fesa/core/Node.hpp index e77398e..44f3e6e 100644 --- a/include/fesa/core/Node.hpp +++ b/include/fesa/core/Node.hpp @@ -10,6 +10,8 @@ class Node { public: Node(NodeId id, double x, double y, double z); + static constexpr std::size_t dofCount() noexcept { return kDofPerNode; } + NodeId id() const noexcept; double x() const noexcept; double y() const noexcept; diff --git a/include/fesa/element/Element.hpp b/include/fesa/element/Element.hpp new file mode 100644 index 0000000..1cc3cd8 --- /dev/null +++ b/include/fesa/element/Element.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "fesa/core/ElementDefinition.hpp" + +#include +#include + +namespace fesa::element { + +using fesa::core::ElementId; +using fesa::core::ElementType; +using fesa::core::NodeId; +using fesa::core::PropertyId; + +class Element { +public: + virtual ~Element() = default; + + virtual ElementId id() const noexcept = 0; + virtual ElementType type() const noexcept = 0; + virtual std::size_t nodeCount() const noexcept = 0; + virtual const std::array& connectivity() const noexcept = 0; + virtual PropertyId propertyId() const noexcept = 0; +}; + +} // namespace fesa::element diff --git a/include/fesa/element/Mitc4Element.hpp b/include/fesa/element/Mitc4Element.hpp new file mode 100644 index 0000000..57d5326 --- /dev/null +++ b/include/fesa/element/Mitc4Element.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "fesa/element/Element.hpp" + +namespace fesa::element { + +class Mitc4Element final : public Element { +public: + Mitc4Element(ElementId id, std::array connectivity, PropertyId property_id); + + ElementId id() const noexcept override; + ElementType type() const noexcept override; + std::size_t nodeCount() const noexcept override; + std::size_t dofCount() const noexcept; + const std::array& connectivity() const noexcept override; + PropertyId propertyId() const noexcept override; + +private: + ElementId id_; + std::array connectivity_; + PropertyId property_id_; +}; + +} // namespace fesa::element diff --git a/include/fesa/load/Load.hpp b/include/fesa/load/Load.hpp new file mode 100644 index 0000000..24047c9 --- /dev/null +++ b/include/fesa/load/Load.hpp @@ -0,0 +1,16 @@ +#pragma once + +namespace fesa::load { + +enum class LoadKind { + Nodal +}; + +class Load { +public: + virtual ~Load() = default; + + virtual LoadKind kind() const noexcept = 0; +}; + +} // namespace fesa::load diff --git a/include/fesa/load/NodalLoad.hpp b/include/fesa/load/NodalLoad.hpp new file mode 100644 index 0000000..6ab0b2b --- /dev/null +++ b/include/fesa/load/NodalLoad.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" +#include "fesa/load/Load.hpp" + +namespace fesa::load { + +using fesa::core::Dof; +using fesa::core::NodeId; + +class NodalLoad final : public Load { +public: + NodalLoad(NodeId node_id, Dof dof, double value); + + LoadKind kind() const noexcept override; + NodeId nodeId() const noexcept; + Dof dof() const noexcept; + double value() const noexcept; + +private: + NodeId node_id_; + Dof dof_; + double value_; +}; + +} // namespace fesa::load diff --git a/include/fesa/material/LinearElasticMaterial.hpp b/include/fesa/material/LinearElasticMaterial.hpp new file mode 100644 index 0000000..9fff542 --- /dev/null +++ b/include/fesa/material/LinearElasticMaterial.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "fesa/material/Material.hpp" + +namespace fesa::material { + +class LinearElasticMaterial final : public Material { +public: + LinearElasticMaterial(MaterialId id, double young_modulus, double poisson_ratio); + + MaterialId id() const noexcept override; + double youngModulus() const noexcept; + double poissonRatio() const noexcept; + +private: + MaterialId id_; + double young_modulus_; + double poisson_ratio_; +}; + +} // namespace fesa::material diff --git a/include/fesa/material/Material.hpp b/include/fesa/material/Material.hpp new file mode 100644 index 0000000..907c65c --- /dev/null +++ b/include/fesa/material/Material.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" + +namespace fesa::material { + +using fesa::core::MaterialId; + +class Material { +public: + virtual ~Material() = default; + + virtual MaterialId id() const noexcept = 0; +}; + +} // namespace fesa::material diff --git a/include/fesa/property/ShellProperty.hpp b/include/fesa/property/ShellProperty.hpp new file mode 100644 index 0000000..0743ce1 --- /dev/null +++ b/include/fesa/property/ShellProperty.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include "fesa/core/ModelTypes.hpp" + +namespace fesa::property { + +using fesa::core::MaterialId; +using fesa::core::PropertyId; + +class ShellProperty { +public: + ShellProperty(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::property diff --git a/phases/analysis-model-objects/index.json b/phases/analysis-model-objects/index.json new file mode 100644 index 0000000..d516078 --- /dev/null +++ b/phases/analysis-model-objects/index.json @@ -0,0 +1,72 @@ +{ + "project": "FESA Structural Solver", + "phase": "analysis-model-objects", + "steps": [ + { + "step": 0, + "name": "model-object-contract", + "status": "completed", + "summary": "Created analysis model object implementation plan with base-class contracts, exclusions, and TDD traceability." + }, + { + "step": 1, + "name": "node-model-class", + "status": "completed", + "summary": "Added Node DOF count contract and tests while keeping Node free of solver state." + }, + { + "step": 2, + "name": "element-base-interface", + "status": "completed", + "summary": "Added identity-only Element base interface and model-object CTest target." + }, + { + "step": 3, + "name": "mitc4-element-model", + "status": "completed", + "summary": "Added MITC4 element model skeleton with four-node connectivity and 24-DOF count tests." + }, + { + "step": 4, + "name": "material-base-interface", + "status": "completed", + "summary": "Added Material base and LinearElasticMaterial model object with ownership tests." + }, + { + "step": 5, + "name": "property-section-model", + "status": "completed", + "summary": "Added ShellProperty model object with positive-thickness validation tests." + }, + { + "step": 6, + "name": "load-base-interface", + "status": "completed", + "summary": "Added Load base and NodalLoad model object with polymorphic access tests." + }, + { + "step": 7, + "name": "boundary-base-interface", + "status": "completed", + "summary": "Added BoundaryCondition base and SinglePointConstraint model object with polymorphic access tests." + }, + { + "step": 8, + "name": "domain-polymorphic-ownership", + "status": "completed", + "summary": "Extended Domain with unique_ptr ownership APIs for element, material, load, and boundary model objects while preserving existing definition APIs." + }, + { + "step": 9, + "name": "legacy-definition-compatibility", + "status": "completed", + "summary": "Documented compatibility between core definition DTOs and new runtime model object ownership APIs." + }, + { + "step": 10, + "name": "validation-report-handoff", + "status": "completed", + "summary": "Recorded build/test evidence, updated project progress, and marked the phase completed." + } + ] +} diff --git a/phases/analysis-model-objects/step0.md b/phases/analysis-model-objects/step0.md new file mode 100644 index 0000000..5331a27 --- /dev/null +++ b/phases/analysis-model-objects/step0.md @@ -0,0 +1,54 @@ +# Step 0: model-object-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/domain-model-foundation-implementation-plan.md` +- `/include/fesa/core/Domain.hpp` + +## Task + +Create `/docs/implementation-plans/analysis-model-objects-implementation-plan.md`. + +The document must define the model object layer that will be stored by `Domain`: + +- `Node` model object. +- abstract `Element` base and MITC4 model object skeleton. +- abstract `Material` base and linear elastic material model object. +- shell property/section model object. +- abstract `Load` base and nodal load model object. +- abstract `BoundaryCondition` base and single-point constraint model object. +- `Domain` RAII ownership of polymorphic model objects through `std::unique_ptr`. + +State explicitly that this phase does not implement MITC4 stiffness, material constitutive matrices, global force assembly, boundary-condition matrix application, equation numbering, solver vectors, HDF5 output, reference comparison, or reference-solver execution. + +Document how existing `*Definition` value objects remain compatible as input DTOs until parser/factory work replaces or maps them. + +## Tests To Write First + +This is a documentation planning step. Do not write C++ production code in this step. + +## Acceptance Criteria + +Run: + +```powershell +python -m unittest discover -s scripts -p "test_*.py" +python scripts/validate_workspace.py +``` + +Update `/phases/analysis-model-objects/index.json` step 0 with `completed`, `error`, or `blocked`. + +## Do Not + +- Do not change C++ code in this step. +- Do not change MITC4 formulation, I/O contracts, reference artifacts, or tolerance policy. diff --git a/phases/analysis-model-objects/step1.md b/phases/analysis-model-objects/step1.md new file mode 100644 index 0000000..24e6991 --- /dev/null +++ b/phases/analysis-model-objects/step1.md @@ -0,0 +1,53 @@ +# Step 1: node-model-class + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/Node.hpp` +- `/tests/core/node_test.cpp` + +## Task + +Refine the existing `Node` model class contract without adding solver state. + +Allowed files: + +- Modify `/include/fesa/core/Node.hpp` +- Modify `/src/core/Domain.cpp` only if constructor/accessor definitions move or change +- Modify `/tests/core/node_test.cpp` + +Required behavior: + +- `Node` exposes `id()`, `coordinates()`, `x()`, `y()`, `z()`. +- `Node` exposes `static constexpr std::size_t dofCount()` or equivalent compile-time DOF count of 6. +- `Node` does not store equation ids, displacements, residuals, reactions, or constraints. + +## Tests To Write First + +Extend `/tests/core/node_test.cpp` before production changes: + +- Verify `Node::dofCount() == 6`. +- Verify coordinate order is preserved. +- Verify the returned coordinate data is const from a const `Node`. + +Run the targeted build and confirm RED before implementation. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain +``` + +Update `/phases/analysis-model-objects/index.json` step 1 with status and summary. + +## Do Not + +- Do not add equation ids or solver state to `Node`. diff --git a/phases/analysis-model-objects/step10.md b/phases/analysis-model-objects/step10.md new file mode 100644 index 0000000..3151164 --- /dev/null +++ b/phases/analysis-model-objects/step10.md @@ -0,0 +1,54 @@ +# Step 10: 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/analysis-model-objects/index.json` + +## Task + +Run final verification and write handoff evidence. + +Allowed files: + +- Create `/docs/build-test-reports/analysis-model-objects-build-test.md` +- Modify `/docs/PROGRESS.md` +- Modify `/docs/WORKNOTE.md` only if new traps or failed approaches occurred +- Modify `/phases/analysis-model-objects/index.json` +- Modify `/phases/index.json` + +The build/test report must include command log summary, exit codes, configure/build/CTest status, failure classification or `N/A`, and no-change assertions for reference artifacts and tolerance policy. + +## 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|model-object" +git diff --check +``` + +Update: + +- `/phases/analysis-model-objects/index.json` step 10 with `completed`, `error`, or `blocked`. +- `/phases/index.json` phase status to `completed` only if all steps are completed. +- `/docs/PROGRESS.md` with validation evidence. + +## Do Not + +- Do not claim MITC4 numerical implementation, reference comparison success, or release readiness. +- Do not run Abaqus, Nastran, or any reference solver. diff --git a/phases/analysis-model-objects/step2.md b/phases/analysis-model-objects/step2.md new file mode 100644 index 0000000..ae1e755 --- /dev/null +++ b/phases/analysis-model-objects/step2.md @@ -0,0 +1,58 @@ +# Step 2: element-base-interface + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/ElementDefinition.hpp` +- `/CMakeLists.txt` + +## Task + +Add the abstract element model interface. + +Allowed files: + +- Create `/include/fesa/element/Element.hpp` +- Create `/tests/element/element_base_test.cpp` +- Modify `/tests/core/domain_bootstrap_test.cpp` only if the current test executable remains the single test runner +- Modify `/CMakeLists.txt` + +Required interface: + +- virtual destructor. +- `ElementId id() const noexcept`. +- `ElementType type() const noexcept`. +- `std::size_t nodeCount() const noexcept`. +- `std::span` is not allowed because this project targets C++17. +- Use const reference return for connectivity data. +- no stiffness, residual, stress, state, or equation API in this phase. + +## Tests To Write First + +Write `/tests/element/element_base_test.cpp` before production changes. Use a small test subclass to prove: + +- base pointer dispatch works. +- id/type/connectivity/property id are readable through base API. +- deleting through `std::unique_ptr` is valid. + +Run targeted build and confirm RED because `Element.hpp` is missing. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 2 status. + +## Do Not + +- Do not implement MITC4 stiffness or numerical element behavior. diff --git a/phases/analysis-model-objects/step3.md b/phases/analysis-model-objects/step3.md new file mode 100644 index 0000000..8d953b2 --- /dev/null +++ b/phases/analysis-model-objects/step3.md @@ -0,0 +1,57 @@ +# Step 3: mitc4-element-model + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/element/Element.hpp` +- `/include/fesa/core/ElementDefinition.hpp` + +## Task + +Add a MITC4 element model object skeleton. + +Allowed files: + +- Create `/include/fesa/element/Mitc4Element.hpp` +- Create `/src/element/Mitc4Element.cpp` +- Create `/tests/element/mitc4_element_model_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- `Mitc4Element` derives from `Element`. +- Stores element id, four node ids, and property id. +- Reports `ElementType::Mitc4`. +- Reports `nodeCount() == 4`. +- Reports `dofCount() == 24`. +- Preserves connectivity order. + +## Tests To Write First + +Write `/tests/element/mitc4_element_model_test.cpp` before production changes: + +- Construct `Mitc4Element`. +- Access it through `const Element&`. +- Verify id, type, node count, DOF count, connectivity, and property id. + +Run targeted build and confirm RED because `Mitc4Element.hpp` is missing. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 3 status. + +## Do Not + +- Do not implement element stiffness, Jacobians, MITC shear interpolation, resultants, stress, or integration-point state. diff --git a/phases/analysis-model-objects/step4.md b/phases/analysis-model-objects/step4.md new file mode 100644 index 0000000..ce737e9 --- /dev/null +++ b/phases/analysis-model-objects/step4.md @@ -0,0 +1,55 @@ +# Step 4: material-base-interface + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/MaterialDefinition.hpp` + +## Task + +Add material model base and linear elastic material model object. + +Allowed files: + +- Create `/include/fesa/material/Material.hpp` +- Create `/include/fesa/material/LinearElasticMaterial.hpp` +- Create `/src/material/LinearElasticMaterial.cpp` +- Create `/tests/material/material_base_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- `Material` has virtual destructor and `MaterialId id() const noexcept`. +- `LinearElasticMaterial` derives from `Material`. +- Stores Young's modulus and Poisson ratio. +- No constitutive matrix or stress update in this phase. + +## Tests To Write First + +Write `/tests/material/material_base_test.cpp` before production changes: + +- Access `LinearElasticMaterial` through `const Material&`. +- Verify id, E, and nu. +- Verify deletion through `std::unique_ptr`. + +Run targeted build and confirm RED. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 4 status. + +## Do Not + +- Do not implement constitutive matrices, stress updates, or tangent stiffness. diff --git a/phases/analysis-model-objects/step5.md b/phases/analysis-model-objects/step5.md new file mode 100644 index 0000000..891d831 --- /dev/null +++ b/phases/analysis-model-objects/step5.md @@ -0,0 +1,53 @@ +# Step 5: property-section-model + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/PropertyDefinition.hpp` + +## Task + +Add shell property/section model object. + +Allowed files: + +- Create `/include/fesa/property/ShellProperty.hpp` +- Create `/src/property/ShellProperty.cpp` +- Create `/tests/property/shell_property_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- Stores property id, material id, and thickness. +- Exposes const accessors. +- Rejects non-positive thickness with `std::invalid_argument`. +- Does not compute shell section stiffness. + +## Tests To Write First + +Write `/tests/property/shell_property_test.cpp` before production changes: + +- Valid property stores id/material/thickness. +- Zero or negative thickness throws. + +Run targeted build and confirm RED. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 5 status. + +## Do Not + +- Do not implement membrane, bending, or shear constitutive stiffness. diff --git a/phases/analysis-model-objects/step6.md b/phases/analysis-model-objects/step6.md new file mode 100644 index 0000000..8793b6e --- /dev/null +++ b/phases/analysis-model-objects/step6.md @@ -0,0 +1,55 @@ +# Step 6: load-base-interface + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/LoadDefinition.hpp` + +## Task + +Add load model base and nodal load object. + +Allowed files: + +- Create `/include/fesa/load/Load.hpp` +- Create `/include/fesa/load/NodalLoad.hpp` +- Create `/src/load/NodalLoad.cpp` +- Create `/tests/load/load_base_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- `Load` has virtual destructor and a load kind enum. +- `NodalLoad` derives from `Load`. +- Stores node id, DOF, and value. +- No global vector assembly in this phase. + +## Tests To Write First + +Write `/tests/load/load_base_test.cpp` before production changes: + +- Access `NodalLoad` through `const Load&`. +- Verify kind, node id, DOF, value. +- Verify deletion through `std::unique_ptr`. + +Run targeted build and confirm RED. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 6 status. + +## Do Not + +- Do not assemble forces or apply time/load factors. diff --git a/phases/analysis-model-objects/step7.md b/phases/analysis-model-objects/step7.md new file mode 100644 index 0000000..ef754d8 --- /dev/null +++ b/phases/analysis-model-objects/step7.md @@ -0,0 +1,55 @@ +# Step 7: boundary-base-interface + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/BoundaryCondition.hpp` + +## Task + +Add boundary condition model base and single-point constraint object. + +Allowed files: + +- Create `/include/fesa/boundary/BoundaryCondition.hpp` +- Create `/include/fesa/boundary/SinglePointConstraint.hpp` +- Create `/src/boundary/SinglePointConstraint.cpp` +- Create `/tests/boundary/boundary_base_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- `BoundaryCondition` has virtual destructor and a boundary kind enum. +- `SinglePointConstraint` derives from `BoundaryCondition`. +- Stores node id, DOF, prescribed value. +- No matrix elimination or reaction recovery in this phase. + +## Tests To Write First + +Write `/tests/boundary/boundary_base_test.cpp` before production changes: + +- Access `SinglePointConstraint` through `const BoundaryCondition&`. +- Verify kind, node id, DOF, value. +- Verify deletion through `std::unique_ptr`. + +Run targeted build and confirm RED. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +``` + +Update step 7 status. + +## Do Not + +- Do not apply constraints to matrices or compute reactions. diff --git a/phases/analysis-model-objects/step8.md b/phases/analysis-model-objects/step8.md new file mode 100644 index 0000000..be0cc5d --- /dev/null +++ b/phases/analysis-model-objects/step8.md @@ -0,0 +1,62 @@ +# Step 8: domain-polymorphic-ownership + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/Domain.hpp` +- all model object headers created by previous steps + +## Task + +Extend `Domain` with RAII ownership APIs for polymorphic model objects while preserving existing definition-storage tests. + +Allowed files: + +- Modify `/include/fesa/core/Domain.hpp` +- Modify `/src/core/Domain.cpp` +- Create `/tests/core/domain_model_object_test.cpp` +- Modify `/tests/core/domain_bootstrap_test.cpp` +- Modify `/CMakeLists.txt` + +Required behavior: + +- `Domain::addElementObject(std::unique_ptr)` +- `Domain::findElementObject(ElementId)` and `Domain::elementObject(ElementId)` +- equivalent add/find/direct APIs for `Material`, `Load`, and model `BoundaryCondition`. +- duplicate ids or duplicate load/boundary keys throw `std::invalid_argument`. +- missing direct lookup throws `std::out_of_range`. +- retrieval returns const base references or const base pointers. +- existing `Definition` APIs remain available. + +## Tests To Write First + +Write `/tests/core/domain_model_object_test.cpp` before production changes: + +- Domain owns `Mitc4Element`, `LinearElasticMaterial`, `NodalLoad`, and `SinglePointConstraint` through `std::unique_ptr`. +- Find/direct lookup through base APIs works. +- Duplicate ids throw for element/material. +- Missing direct lookup throws. + +Run targeted build and confirm RED. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R domain +``` + +Update step 8 status. + +## Do Not + +- Do not remove existing definition APIs in this step. +- Do not add solver state to Domain. diff --git a/phases/analysis-model-objects/step9.md b/phases/analysis-model-objects/step9.md new file mode 100644 index 0000000..9cac2f3 --- /dev/null +++ b/phases/analysis-model-objects/step9.md @@ -0,0 +1,49 @@ +# Step 9: legacy-definition-compatibility + +## Read First + +Read these files before editing: + +- `/AGENTS.md` +- `/docs/AGENT_RULES.md` +- `/docs/ARCHITECTURE.md` +- `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- `/include/fesa/core/Domain.hpp` +- `/tests/core/domain_storage_test.cpp` +- `/tests/core/domain_model_object_test.cpp` + +## Task + +Preserve and document compatibility between existing `*Definition` value objects and new model-object base classes. + +Allowed files: + +- Modify `/docs/implementation-plans/analysis-model-objects-implementation-plan.md` +- Modify tests only if compatibility gaps are found +- Modify production code only for narrow compatibility fixes + +Required behavior: + +- Existing Domain definition-storage tests still pass. +- New model-object ownership tests still pass. +- The implementation plan clearly states that `*Definition` objects remain parser/factory DTOs until the parser phase decides whether to map or replace them. + +## Tests To Write First + +If production code must change, write or extend a failing C++ test before the change. If this is documentation-only, do not add production code. + +## Acceptance Criteria + +Run: + +```powershell +python scripts/validate_workspace.py +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object" +``` + +Update step 9 status. + +## Do Not + +- Do not delete existing `*Definition` headers or tests. +- Do not rewrite parser or factory behavior. diff --git a/phases/index.json b/phases/index.json index ad8f5c8..9b3404a 100644 --- a/phases/index.json +++ b/phases/index.json @@ -3,6 +3,10 @@ { "dir": "domain-model-foundation", "status": "completed" + }, + { + "dir": "analysis-model-objects", + "status": "completed" } ] } diff --git a/src/boundary/SinglePointConstraint.cpp b/src/boundary/SinglePointConstraint.cpp new file mode 100644 index 0000000..70a57ba --- /dev/null +++ b/src/boundary/SinglePointConstraint.cpp @@ -0,0 +1,24 @@ +#include "fesa/boundary/SinglePointConstraint.hpp" + +namespace fesa::boundary { + +SinglePointConstraint::SinglePointConstraint(NodeId node_id, Dof dof, double value) + : node_id_(node_id), dof_(dof), value_(value) {} + +BoundaryConditionKind SinglePointConstraint::kind() const noexcept { + return BoundaryConditionKind::SinglePointConstraint; +} + +NodeId SinglePointConstraint::nodeId() const noexcept { + return node_id_; +} + +Dof SinglePointConstraint::dof() const noexcept { + return dof_; +} + +double SinglePointConstraint::value() const noexcept { + return value_; +} + +} // namespace fesa::boundary diff --git a/src/core/Domain.cpp b/src/core/Domain.cpp index b9789e3..7a9a986 100644 --- a/src/core/Domain.cpp +++ b/src/core/Domain.cpp @@ -1,5 +1,8 @@ #include "fesa/core/Domain.hpp" +#include "fesa/boundary/SinglePointConstraint.hpp" +#include "fesa/load/NodalLoad.hpp" + #include #include @@ -243,6 +246,72 @@ void Domain::addStep(LinearStaticStepDefinition step) { steps_.emplace(id, std::move(step)); } +void Domain::addElementObject(std::unique_ptr element) { + if (!element) { + throw std::invalid_argument("element object is null"); + } + const ElementId id = element->id(); + if (element_objects_.find(id) != element_objects_.end()) { + throw std::invalid_argument("duplicate element object id"); + } + for (const NodeId node_id : element->connectivity()) { + if (findNode(node_id) == nullptr) { + throw std::invalid_argument("element object references missing node id"); + } + } + element_objects_.emplace(id, std::move(element)); +} + +void Domain::addMaterialObject(std::unique_ptr material) { + if (!material) { + throw std::invalid_argument("material object is null"); + } + const MaterialId id = material->id(); + const auto inserted = material_objects_.emplace(id, std::move(material)); + if (!inserted.second) { + throw std::invalid_argument("duplicate material object id"); + } +} + +std::size_t Domain::addLoadObject(std::unique_ptr load) { + if (!load) { + throw std::invalid_argument("load object is null"); + } + if (const auto* nodal = dynamic_cast(load.get())) { + for (const auto& existing : load_objects_) { + const auto* existing_nodal = dynamic_cast(existing.get()); + if (existing_nodal != nullptr && + existing_nodal->nodeId() == nodal->nodeId() && + existing_nodal->dof() == nodal->dof()) { + throw std::invalid_argument("duplicate nodal load key"); + } + } + } + const std::size_t index = load_objects_.size(); + load_objects_.push_back(std::move(load)); + return index; +} + +std::size_t Domain::addBoundaryObject(std::unique_ptr boundary) { + if (!boundary) { + throw std::invalid_argument("boundary object is null"); + } + if (const auto* spc = dynamic_cast(boundary.get())) { + for (const auto& existing : boundary_objects_) { + const auto* existing_spc = + dynamic_cast(existing.get()); + if (existing_spc != nullptr && + existing_spc->nodeId() == spc->nodeId() && + existing_spc->dof() == spc->dof()) { + throw std::invalid_argument("duplicate boundary object key"); + } + } + } + const std::size_t index = boundary_objects_.size(); + boundary_objects_.push_back(std::move(boundary)); + return index; +} + const Node* Domain::findNode(NodeId id) const noexcept { const auto it = nodes_.find(id); return it == nodes_.end() ? nullptr : &it->second; @@ -378,4 +447,70 @@ std::size_t Domain::stepCount() const noexcept { return steps_.size(); } +const fesa::element::Element* Domain::findElementObject(ElementId id) const noexcept { + const auto it = element_objects_.find(id); + return it == element_objects_.end() ? nullptr : it->second.get(); +} + +const fesa::element::Element& Domain::elementObject(ElementId id) const { + const fesa::element::Element* found = findElementObject(id); + if (found == nullptr) { + throw std::out_of_range("element object id not found"); + } + return *found; +} + +std::size_t Domain::elementObjectCount() const noexcept { + return element_objects_.size(); +} + +const fesa::material::Material* Domain::findMaterialObject(MaterialId id) const noexcept { + const auto it = material_objects_.find(id); + return it == material_objects_.end() ? nullptr : it->second.get(); +} + +const fesa::material::Material& Domain::materialObject(MaterialId id) const { + const fesa::material::Material* found = findMaterialObject(id); + if (found == nullptr) { + throw std::out_of_range("material object id not found"); + } + return *found; +} + +std::size_t Domain::materialObjectCount() const noexcept { + return material_objects_.size(); +} + +const fesa::load::Load* Domain::findLoadObject(std::size_t index) const noexcept { + return index < load_objects_.size() ? load_objects_[index].get() : nullptr; +} + +const fesa::load::Load& Domain::loadObject(std::size_t index) const { + const fesa::load::Load* found = findLoadObject(index); + if (found == nullptr) { + throw std::out_of_range("load object index not found"); + } + return *found; +} + +std::size_t Domain::loadObjectCount() const noexcept { + return load_objects_.size(); +} + +const fesa::boundary::BoundaryCondition* Domain::findBoundaryObject(std::size_t index) const noexcept { + return index < boundary_objects_.size() ? boundary_objects_[index].get() : nullptr; +} + +const fesa::boundary::BoundaryCondition& Domain::boundaryObject(std::size_t index) const { + const fesa::boundary::BoundaryCondition* found = findBoundaryObject(index); + if (found == nullptr) { + throw std::out_of_range("boundary object index not found"); + } + return *found; +} + +std::size_t Domain::boundaryObjectCount() const noexcept { + return boundary_objects_.size(); +} + } // namespace fesa::core diff --git a/src/element/Mitc4Element.cpp b/src/element/Mitc4Element.cpp new file mode 100644 index 0000000..0e04c1c --- /dev/null +++ b/src/element/Mitc4Element.cpp @@ -0,0 +1,37 @@ +#include "fesa/element/Mitc4Element.hpp" + +#include "fesa/core/Node.hpp" + +namespace fesa::element { + +Mitc4Element::Mitc4Element( + ElementId id, + std::array connectivity, + PropertyId property_id) + : id_(id), connectivity_(connectivity), property_id_(property_id) {} + +ElementId Mitc4Element::id() const noexcept { + return id_; +} + +ElementType Mitc4Element::type() const noexcept { + return ElementType::Mitc4; +} + +std::size_t Mitc4Element::nodeCount() const noexcept { + return connectivity_.size(); +} + +std::size_t Mitc4Element::dofCount() const noexcept { + return connectivity_.size() * fesa::core::Node::dofCount(); +} + +const std::array& Mitc4Element::connectivity() const noexcept { + return connectivity_; +} + +PropertyId Mitc4Element::propertyId() const noexcept { + return property_id_; +} + +} // namespace fesa::element diff --git a/src/load/NodalLoad.cpp b/src/load/NodalLoad.cpp new file mode 100644 index 0000000..0fb10ab --- /dev/null +++ b/src/load/NodalLoad.cpp @@ -0,0 +1,24 @@ +#include "fesa/load/NodalLoad.hpp" + +namespace fesa::load { + +NodalLoad::NodalLoad(NodeId node_id, Dof dof, double value) + : node_id_(node_id), dof_(dof), value_(value) {} + +LoadKind NodalLoad::kind() const noexcept { + return LoadKind::Nodal; +} + +NodeId NodalLoad::nodeId() const noexcept { + return node_id_; +} + +Dof NodalLoad::dof() const noexcept { + return dof_; +} + +double NodalLoad::value() const noexcept { + return value_; +} + +} // namespace fesa::load diff --git a/src/material/LinearElasticMaterial.cpp b/src/material/LinearElasticMaterial.cpp new file mode 100644 index 0000000..695bf00 --- /dev/null +++ b/src/material/LinearElasticMaterial.cpp @@ -0,0 +1,23 @@ +#include "fesa/material/LinearElasticMaterial.hpp" + +namespace fesa::material { + +LinearElasticMaterial::LinearElasticMaterial( + MaterialId id, + double young_modulus, + double poisson_ratio) + : id_(id), young_modulus_(young_modulus), poisson_ratio_(poisson_ratio) {} + +MaterialId LinearElasticMaterial::id() const noexcept { + return id_; +} + +double LinearElasticMaterial::youngModulus() const noexcept { + return young_modulus_; +} + +double LinearElasticMaterial::poissonRatio() const noexcept { + return poisson_ratio_; +} + +} // namespace fesa::material diff --git a/src/property/ShellProperty.cpp b/src/property/ShellProperty.cpp new file mode 100644 index 0000000..2b2104c --- /dev/null +++ b/src/property/ShellProperty.cpp @@ -0,0 +1,26 @@ +#include "fesa/property/ShellProperty.hpp" + +#include + +namespace fesa::property { + +ShellProperty::ShellProperty(PropertyId id, MaterialId material_id, double thickness) + : id_(id), material_id_(material_id), thickness_(thickness) { + if (thickness <= 0.0) { + throw std::invalid_argument("shell property thickness must be positive"); + } +} + +PropertyId ShellProperty::id() const noexcept { + return id_; +} + +MaterialId ShellProperty::materialId() const noexcept { + return material_id_; +} + +double ShellProperty::thickness() const noexcept { + return thickness_; +} + +} // namespace fesa::property diff --git a/tests/boundary/boundary_base_test.cpp b/tests/boundary/boundary_base_test.cpp new file mode 100644 index 0000000..665341d --- /dev/null +++ b/tests/boundary/boundary_base_test.cpp @@ -0,0 +1,31 @@ +#include "fesa/boundary/BoundaryCondition.hpp" +#include "fesa/boundary/SinglePointConstraint.hpp" + +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +} // namespace + +int run_boundary_base_tests() { + std::unique_ptr owned = + std::make_unique(1, fesa::core::Dof::UR3, 0.25); + const fesa::boundary::BoundaryCondition& boundary = *owned; + const auto& spc = static_cast(boundary); + + if (const int result = require(boundary.kind() == fesa::boundary::BoundaryConditionKind::SinglePointConstraint); + result != 0) { + return result; + } + if (const int result = require(spc.nodeId() == 1); result != 0) { + return result; + } + if (const int result = require(spc.dof() == fesa::core::Dof::UR3); result != 0) { + return result; + } + return require(spc.value() == 0.25); +} diff --git a/tests/core/domain_bootstrap_test.cpp b/tests/core/domain_bootstrap_test.cpp index 9e2aac7..773410f 100644 --- a/tests/core/domain_bootstrap_test.cpp +++ b/tests/core/domain_bootstrap_test.cpp @@ -1,4 +1,5 @@ int run_boundary_condition_tests(); +int run_domain_model_object_tests(); int run_domain_storage_tests(); int run_element_definition_tests(); int run_load_definition_tests(); @@ -27,6 +28,9 @@ int main() { if (const int result = run_boundary_condition_tests(); result != 0) { return result; } + if (const int result = run_domain_model_object_tests(); result != 0) { + return result; + } if (const int result = run_load_definition_tests(); result != 0) { return result; } diff --git a/tests/core/domain_model_object_test.cpp b/tests/core/domain_model_object_test.cpp new file mode 100644 index 0000000..4c0d92a --- /dev/null +++ b/tests/core/domain_model_object_test.cpp @@ -0,0 +1,169 @@ +#include "fesa/boundary/SinglePointConstraint.hpp" +#include "fesa/core/Domain.hpp" +#include "fesa/element/Mitc4Element.hpp" +#include "fesa/load/NodalLoad.hpp" +#include "fesa/material/LinearElasticMaterial.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; +} + +fesa::core::Domain populated_domain() { + 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}); + return domain; +} + +int domain_owns_element_and_material_objects() { + fesa::core::Domain domain = populated_domain(); + + domain.addElementObject(std::make_unique( + 100, + std::array{1, 2, 3, 4}, + 500)); + domain.addMaterialObject(std::make_unique(700, 210.0, 0.3)); + + const fesa::element::Element* element = domain.findElementObject(100); + if (const int result = require(element != nullptr); result != 0) { + return result; + } + if (const int result = require(element->type() == fesa::core::ElementType::Mitc4); result != 0) { + return result; + } + if (const int result = require(domain.elementObject(100).propertyId() == 500); result != 0) { + return result; + } + + const fesa::material::Material* material = domain.findMaterialObject(700); + if (const int result = require(material != nullptr); result != 0) { + return result; + } + return require(domain.materialObject(700).id() == 700); +} + +int duplicate_element_and_material_object_ids_throw() { + fesa::core::Domain domain = populated_domain(); + domain.addElementObject(std::make_unique( + 100, + std::array{1, 2, 3, 4}, + 500)); + domain.addMaterialObject(std::make_unique(700, 210.0, 0.3)); + + if (const int result = require_throws([&domain]() { + domain.addElementObject(std::make_unique( + 100, + std::array{1, 2, 3, 4}, + 500)); + }); + result != 0) { + return result; + } + return require_throws([&domain]() { + domain.addMaterialObject(std::make_unique(700, 100.0, 0.25)); + }); +} + +int missing_model_object_direct_lookup_throws() { + const fesa::core::Domain domain; + + if (const int result = require(domain.findElementObject(404) == nullptr); result != 0) { + return result; + } + if (const int result = require(domain.findMaterialObject(404) == nullptr); result != 0) { + return result; + } + if (const int result = require_throws([&domain]() { + (void)domain.elementObject(404); + }); + result != 0) { + return result; + } + return require_throws([&domain]() { + (void)domain.materialObject(404); + }); +} + +int domain_owns_load_and_boundary_objects_by_index() { + fesa::core::Domain domain = populated_domain(); + + const std::size_t load_index = domain.addLoadObject( + std::make_unique(1, fesa::core::Dof::U3, -100.0)); + const std::size_t boundary_index = domain.addBoundaryObject( + std::make_unique(1, fesa::core::Dof::U1, 0.0)); + + if (const int result = require(load_index == 0); result != 0) { + return result; + } + if (const int result = require(boundary_index == 0); result != 0) { + return result; + } + if (const int result = require(domain.findLoadObject(load_index) != nullptr); result != 0) { + return result; + } + if (const int result = require(domain.findBoundaryObject(boundary_index) != nullptr); result != 0) { + return result; + } + if (const int result = require(domain.loadObject(load_index).kind() == fesa::load::LoadKind::Nodal); result != 0) { + return result; + } + return require( + domain.boundaryObject(boundary_index).kind() == + fesa::boundary::BoundaryConditionKind::SinglePointConstraint); +} + +int duplicate_load_and_boundary_keys_throw() { + fesa::core::Domain domain = populated_domain(); + domain.addLoadObject(std::make_unique(1, fesa::core::Dof::U3, -100.0)); + domain.addBoundaryObject(std::make_unique(1, fesa::core::Dof::U1, 0.0)); + + if (const int result = require_throws([&domain]() { + domain.addLoadObject(std::make_unique(1, fesa::core::Dof::U3, -200.0)); + }); + result != 0) { + return result; + } + return require_throws([&domain]() { + domain.addBoundaryObject(std::make_unique(1, fesa::core::Dof::U1, 1.0)); + }); +} + +} // namespace + +int run_domain_model_object_tests() { + if (const int result = domain_owns_element_and_material_objects(); result != 0) { + return result; + } + if (const int result = duplicate_element_and_material_object_ids_throw(); result != 0) { + return result; + } + if (const int result = missing_model_object_direct_lookup_throws(); result != 0) { + return result; + } + if (const int result = domain_owns_load_and_boundary_objects_by_index(); result != 0) { + return result; + } + if (const int result = duplicate_load_and_boundary_keys_throw(); result != 0) { + return result; + } + return 0; +} diff --git a/tests/core/node_test.cpp b/tests/core/node_test.cpp index 750e5d3..869dc2f 100644 --- a/tests/core/node_test.cpp +++ b/tests/core/node_test.cpp @@ -1,5 +1,8 @@ #include "fesa/core/Node.hpp" +#include +#include + namespace { int require(bool condition) { @@ -11,6 +14,9 @@ int require(bool condition) { int run_node_tests() { const fesa::core::Node node{42, 1.0, 2.0, 3.0}; + if (const int result = require(fesa::core::Node::dofCount() == 6); result != 0) { + return result; + } if (const int result = require(node.id() == 42); result != 0) { return result; } @@ -32,6 +38,10 @@ int run_node_tests() { if (const int result = require(node.coordinates()[2] == 3.0); result != 0) { return result; } + if (const int result = require((std::is_same&>::value)); + result != 0) { + return result; + } return 0; } diff --git a/tests/element/element_base_test.cpp b/tests/element/element_base_test.cpp new file mode 100644 index 0000000..6d14d8f --- /dev/null +++ b/tests/element/element_base_test.cpp @@ -0,0 +1,51 @@ +#include "fesa/element/Element.hpp" + +#include +#include + +namespace { + +using fesa::element::ElementId; +using fesa::element::ElementType; +using fesa::element::NodeId; +using fesa::element::PropertyId; + +int require(bool condition) { + return condition ? 0 : 1; +} + +class TestElement final : public fesa::element::Element { +public: + ElementId id() const noexcept override { return 100; } + ElementType type() const noexcept override { return ElementType::Mitc4; } + std::size_t nodeCount() const noexcept override { return connectivity_.size(); } + const std::array& connectivity() const noexcept override { return connectivity_; } + PropertyId propertyId() const noexcept override { return 500; } + +private: + std::array connectivity_{1, 2, 3, 4}; +}; + +} // namespace + +int run_element_base_tests() { + std::unique_ptr owned = std::make_unique(); + const fesa::element::Element& element = *owned; + + 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.nodeCount() == 4); result != 0) { + return result; + } + if (const int result = require(element.connectivity()[0] == 1); result != 0) { + return result; + } + if (const int result = require(element.connectivity()[3] == 4); result != 0) { + return result; + } + return require(element.propertyId() == 500); +} diff --git a/tests/element/mitc4_element_model_test.cpp b/tests/element/mitc4_element_model_test.cpp new file mode 100644 index 0000000..d4b9aa5 --- /dev/null +++ b/tests/element/mitc4_element_model_test.cpp @@ -0,0 +1,40 @@ +#include "fesa/element/Mitc4Element.hpp" + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +} // namespace + +int run_mitc4_element_model_tests() { + const fesa::element::Mitc4Element mitc4{100, {1, 2, 3, 4}, 500}; + const fesa::element::Element& element = mitc4; + + 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.nodeCount() == 4); result != 0) { + return result; + } + if (const int result = require(mitc4.dofCount() == 24); 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; + } + return require(element.propertyId() == 500); +} diff --git a/tests/load/load_base_test.cpp b/tests/load/load_base_test.cpp new file mode 100644 index 0000000..1a90937 --- /dev/null +++ b/tests/load/load_base_test.cpp @@ -0,0 +1,30 @@ +#include "fesa/load/Load.hpp" +#include "fesa/load/NodalLoad.hpp" + +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +} // namespace + +int run_load_base_tests() { + std::unique_ptr owned = + std::make_unique(1, fesa::core::Dof::U3, -100.0); + const fesa::load::Load& load = *owned; + const auto& nodal = static_cast(load); + + if (const int result = require(load.kind() == fesa::load::LoadKind::Nodal); result != 0) { + return result; + } + if (const int result = require(nodal.nodeId() == 1); result != 0) { + return result; + } + if (const int result = require(nodal.dof() == fesa::core::Dof::U3); result != 0) { + return result; + } + return require(nodal.value() == -100.0); +} diff --git a/tests/material/material_base_test.cpp b/tests/material/material_base_test.cpp new file mode 100644 index 0000000..9cb6750 --- /dev/null +++ b/tests/material/material_base_test.cpp @@ -0,0 +1,27 @@ +#include "fesa/material/LinearElasticMaterial.hpp" +#include "fesa/material/Material.hpp" + +#include + +namespace { + +int require(bool condition) { + return condition ? 0 : 1; +} + +} // namespace + +int run_material_base_tests() { + std::unique_ptr owned = + std::make_unique(700, 210.0, 0.3); + const fesa::material::Material& material = *owned; + const auto& elastic = static_cast(material); + + if (const int result = require(material.id() == 700); result != 0) { + return result; + } + if (const int result = require(elastic.youngModulus() == 210.0); result != 0) { + return result; + } + return require(elastic.poissonRatio() == 0.3); +} diff --git a/tests/model_object_main.cpp b/tests/model_object_main.cpp new file mode 100644 index 0000000..277e843 --- /dev/null +++ b/tests/model_object_main.cpp @@ -0,0 +1,28 @@ +int run_boundary_base_tests(); +int run_element_base_tests(); +int run_load_base_tests(); +int run_material_base_tests(); +int run_mitc4_element_model_tests(); +int run_shell_property_tests(); + +int main() { + if (const int result = run_boundary_base_tests(); result != 0) { + return result; + } + if (const int result = run_element_base_tests(); result != 0) { + return result; + } + if (const int result = run_mitc4_element_model_tests(); result != 0) { + return result; + } + if (const int result = run_material_base_tests(); result != 0) { + return result; + } + if (const int result = run_shell_property_tests(); result != 0) { + return result; + } + if (const int result = run_load_base_tests(); result != 0) { + return result; + } + return 0; +} diff --git a/tests/property/shell_property_test.cpp b/tests/property/shell_property_test.cpp new file mode 100644 index 0000000..3d2f248 --- /dev/null +++ b/tests/property/shell_property_test.cpp @@ -0,0 +1,46 @@ +#include "fesa/property/ShellProperty.hpp" + +#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; +} + +} // namespace + +int run_shell_property_tests() { + const fesa::property::ShellProperty 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; + } + if (const int result = require_throws([]() { + (void)fesa::property::ShellProperty{501, 700, 0.0}; + }); + result != 0) { + return result; + } + return require_throws([]() { + (void)fesa::property::ShellProperty{502, 700, -0.01}; + }); +}