feat: add property model foundation
This commit is contained in:
@@ -10,6 +10,7 @@ add_library(fesa_core STATIC
|
||||
src/boundary/SinglePointConstraint.cpp
|
||||
src/core/Domain.cpp
|
||||
src/element/Mitc4Element.cpp
|
||||
src/io/Hdf5ResultWriter.cpp
|
||||
src/load/NodalLoad.cpp
|
||||
src/material/LinearElasticMaterial.cpp
|
||||
src/property/ShellProperty.cpp
|
||||
@@ -45,9 +46,18 @@ add_executable(fesa_model_object_tests
|
||||
tests/load/load_base_test.cpp
|
||||
tests/material/material_base_test.cpp
|
||||
tests/model_object_main.cpp
|
||||
tests/property/property_base_test.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")
|
||||
|
||||
add_executable(fesa_io_tests
|
||||
tests/io/hdf5_result_writer_test.cpp
|
||||
)
|
||||
target_link_libraries(fesa_io_tests PRIVATE fesa_core)
|
||||
|
||||
add_test(NAME io.hdf5-result-writer COMMAND fesa_io_tests)
|
||||
set_tests_properties(io.hdf5-result-writer PROPERTIES LABELS "io;hdf5")
|
||||
|
||||
@@ -38,6 +38,9 @@
|
||||
- Migrated `Domain` canonical storage away from parser-style definition DTOs to runtime model objects for elements, materials, shell properties, loads, and boundary conditions.
|
||||
- Moved `ElementType` into `ModelTypes.hpp` so runtime element interfaces no longer include `ElementDefinition.hpp`.
|
||||
- Updated Domain storage tests so element sets and linear static step indices validate against runtime element, load, and boundary containers.
|
||||
- Created `docs/implementation-plans/property-model-foundation-implementation-plan.md` and `phases/property-model-foundation/`.
|
||||
- Added runtime `Property` base class and `PropertyKind`, made `ShellProperty` derive from `Property`, and migrated `Domain` property ownership to `std::unique_ptr<Property>`.
|
||||
- Added a minimal `Hdf5ResultWriter` skeleton with path validation only; it does not link HDF5 or write files yet.
|
||||
|
||||
## In Progress
|
||||
- Ready for the next upstream MITC4 stage: new solver feature requirements analysis for `mitc4-linear-static-shell`.
|
||||
@@ -59,6 +62,10 @@
|
||||
- 2026-06-09: After Domain runtime storage migration, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully.
|
||||
- 2026-06-09: After Domain runtime storage migration, `python scripts/validate_workspace.py` configured CMake with Visual Studio 17 2022 x64, built Debug targets, ran CTest, and passed.
|
||||
- 2026-06-09: After Domain runtime storage migration, `git diff --check` passed with only Git line-ending normalization warnings.
|
||||
- 2026-06-09: After property model foundation implementation, `ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object|io"` passed. 3 CTest executables ran successfully.
|
||||
- 2026-06-09: After property model foundation implementation, `python -m unittest discover -s scripts -p "test_*.py"` passed. 89 tests ran successfully.
|
||||
- 2026-06-09: After property 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-09: After property model foundation implementation, `git diff --check` passed with only Git line-ending normalization warnings.
|
||||
- 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.
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
# Property Model Foundation Implementation Plan
|
||||
|
||||
## Objective
|
||||
|
||||
Introduce a runtime `Property` base class for element property and section objects, then make `ShellProperty` and `Domain` use that base class consistently.
|
||||
|
||||
This phase keeps property objects as model data only. It does not implement shell section stiffness, constitutive matrices, assembly, solver logic, HDF5 output, or reference comparison.
|
||||
|
||||
## Phase Overview
|
||||
|
||||
1. `property-base-contract`
|
||||
- Record the runtime property model contract and exclusions.
|
||||
|
||||
2. `property-base-interface`
|
||||
- Add `PropertyKind` and abstract `Property`.
|
||||
- Add unit tests for polymorphic use and virtual deletion.
|
||||
|
||||
3. `shell-property-polymorphism`
|
||||
- Make `ShellProperty` derive from `Property`.
|
||||
- Preserve id, material id, thickness, and positive-thickness validation.
|
||||
|
||||
4. `domain-property-ownership`
|
||||
- Change `Domain` property storage to `std::unique_ptr<Property>`.
|
||||
- Validate duplicate property ids and shell-property material references.
|
||||
- Validate element property references against runtime property storage.
|
||||
|
||||
5. `validation-report-handoff`
|
||||
- Run targeted CTest, harness self-tests, workspace validation, and whitespace checks.
|
||||
- Update handoff documents.
|
||||
|
||||
## Runtime Property Contract
|
||||
|
||||
`Property` represents element property and section data that is owned by `Domain` and referenced by elements through `PropertyId`.
|
||||
|
||||
Required interface:
|
||||
|
||||
```cpp
|
||||
namespace fesa::property {
|
||||
|
||||
enum class PropertyKind {
|
||||
Shell
|
||||
};
|
||||
|
||||
class Property {
|
||||
public:
|
||||
virtual ~Property() = default;
|
||||
|
||||
virtual PropertyId id() const noexcept = 0;
|
||||
virtual PropertyKind kind() const noexcept = 0;
|
||||
};
|
||||
|
||||
} // namespace fesa::property
|
||||
```
|
||||
|
||||
`ShellProperty`:
|
||||
|
||||
- derives from `Property`;
|
||||
- returns `PropertyKind::Shell`;
|
||||
- stores `PropertyId`, `MaterialId`, and thickness;
|
||||
- rejects non-positive thickness;
|
||||
- does not compute shell stiffness in this phase.
|
||||
|
||||
## Domain Contract
|
||||
|
||||
`Domain` stores runtime property objects by ownership:
|
||||
|
||||
```cpp
|
||||
std::unordered_map<PropertyId, std::unique_ptr<fesa::property::Property>>
|
||||
```
|
||||
|
||||
`Domain` validates:
|
||||
|
||||
- null property pointer rejection;
|
||||
- duplicate property id rejection;
|
||||
- missing material id for `ShellProperty`;
|
||||
- missing property id for elements.
|
||||
|
||||
## Hdf5ResultWriter Constraint
|
||||
|
||||
The adjacent `Hdf5ResultWriter` work requested for this slice is limited to a skeleton class only. It may expose construction and basic path access, but it must not link HDF5, create files, define result datasets, write metadata, or claim HDF5 output support.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No MITC4 element formulation.
|
||||
- No shell section stiffness.
|
||||
- No material constitutive behavior.
|
||||
- No DofManager work.
|
||||
- No assembly, solver, sparse matrix, or reaction recovery.
|
||||
- No HDF5 file writing or result schema implementation.
|
||||
- No parser/factory implementation.
|
||||
|
||||
## Tests
|
||||
|
||||
Primary C++ tests:
|
||||
|
||||
- `/tests/property/property_base_test.cpp`
|
||||
- `/tests/property/shell_property_test.cpp`
|
||||
- `/tests/core/domain_storage_test.cpp`
|
||||
- `/tests/core/domain_model_object_test.cpp`
|
||||
|
||||
Required validation:
|
||||
|
||||
```powershell
|
||||
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"
|
||||
python -m unittest discover -s scripts -p "test_*.py"
|
||||
python scripts/validate_workspace.py
|
||||
git diff --check
|
||||
```
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "fesa/element/Element.hpp"
|
||||
#include "fesa/load/Load.hpp"
|
||||
#include "fesa/material/Material.hpp"
|
||||
#include "fesa/property/Property.hpp"
|
||||
#include "fesa/property/ShellProperty.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -22,7 +23,8 @@ public:
|
||||
void addNode(Node node);
|
||||
void addElement(std::unique_ptr<fesa::element::Element> element);
|
||||
void addMaterial(std::unique_ptr<fesa::material::Material> material);
|
||||
void addShellProperty(fesa::property::ShellProperty property);
|
||||
void addProperty(std::unique_ptr<fesa::property::Property> property);
|
||||
void addShellProperty(std::unique_ptr<fesa::property::ShellProperty> property);
|
||||
void addNodeSet(std::string name, std::vector<NodeId> node_ids);
|
||||
void addElementSet(std::string name, std::vector<ElementId> element_ids);
|
||||
std::size_t addBoundaryCondition(std::unique_ptr<fesa::boundary::BoundaryCondition> boundary);
|
||||
@@ -41,6 +43,10 @@ public:
|
||||
const fesa::material::Material& material(MaterialId id) const;
|
||||
std::size_t materialCount() const noexcept;
|
||||
|
||||
const fesa::property::Property* findProperty(PropertyId id) const noexcept;
|
||||
const fesa::property::Property& property(PropertyId id) const;
|
||||
std::size_t propertyCount() const noexcept;
|
||||
|
||||
const fesa::property::ShellProperty* findShellProperty(PropertyId id) const noexcept;
|
||||
const fesa::property::ShellProperty& shellProperty(PropertyId id) const;
|
||||
std::size_t shellPropertyCount() const noexcept;
|
||||
@@ -69,7 +75,7 @@ private:
|
||||
std::unordered_map<NodeId, Node> nodes_;
|
||||
std::unordered_map<ElementId, std::unique_ptr<fesa::element::Element>> elements_;
|
||||
std::unordered_map<MaterialId, std::unique_ptr<fesa::material::Material>> materials_;
|
||||
std::unordered_map<PropertyId, fesa::property::ShellProperty> shell_properties_;
|
||||
std::unordered_map<PropertyId, std::unique_ptr<fesa::property::Property>> properties_;
|
||||
std::unordered_map<std::string, std::vector<NodeId>> node_sets_;
|
||||
std::unordered_map<std::string, std::vector<ElementId>> element_sets_;
|
||||
std::vector<std::unique_ptr<fesa::boundary::BoundaryCondition>> boundary_conditions_;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace fesa::io {
|
||||
|
||||
class Hdf5ResultWriter {
|
||||
public:
|
||||
explicit Hdf5ResultWriter(std::string file_path);
|
||||
|
||||
const std::string& filePath() const noexcept;
|
||||
|
||||
private:
|
||||
std::string file_path_;
|
||||
};
|
||||
|
||||
} // namespace fesa::io
|
||||
@@ -0,0 +1,21 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/core/ModelTypes.hpp"
|
||||
|
||||
namespace fesa::property {
|
||||
|
||||
using fesa::core::PropertyId;
|
||||
|
||||
enum class PropertyKind {
|
||||
Shell
|
||||
};
|
||||
|
||||
class Property {
|
||||
public:
|
||||
virtual ~Property() = default;
|
||||
|
||||
virtual PropertyId id() const noexcept = 0;
|
||||
virtual PropertyKind kind() const noexcept = 0;
|
||||
};
|
||||
|
||||
} // namespace fesa::property
|
||||
@@ -1,17 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/core/ModelTypes.hpp"
|
||||
#include "fesa/property/Property.hpp"
|
||||
|
||||
namespace fesa::property {
|
||||
|
||||
using fesa::core::MaterialId;
|
||||
using fesa::core::PropertyId;
|
||||
|
||||
class ShellProperty {
|
||||
class ShellProperty final : public Property {
|
||||
public:
|
||||
ShellProperty(PropertyId id, MaterialId material_id, double thickness);
|
||||
|
||||
PropertyId id() const noexcept;
|
||||
PropertyId id() const noexcept override;
|
||||
PropertyKind kind() const noexcept override;
|
||||
MaterialId materialId() const noexcept;
|
||||
double thickness() const noexcept;
|
||||
|
||||
|
||||
@@ -11,6 +11,10 @@
|
||||
{
|
||||
"dir": "domain-runtime-storage",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"dir": "property-model-foundation",
|
||||
"status": "completed"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"project": "FESA Structural Solver",
|
||||
"phase": "property-model-foundation",
|
||||
"steps": [
|
||||
{
|
||||
"step": 0,
|
||||
"name": "property-base-contract",
|
||||
"status": "completed",
|
||||
"summary": "Created the property model foundation implementation plan with runtime Property ownership rules and HDF5 skeleton limits."
|
||||
},
|
||||
{
|
||||
"step": 1,
|
||||
"name": "property-base-interface",
|
||||
"status": "completed",
|
||||
"summary": "Added PropertyKind and abstract Property base with polymorphic ownership tests."
|
||||
},
|
||||
{
|
||||
"step": 2,
|
||||
"name": "shell-property-polymorphism",
|
||||
"status": "completed",
|
||||
"summary": "Made ShellProperty derive from Property while preserving material id and positive-thickness validation."
|
||||
},
|
||||
{
|
||||
"step": 3,
|
||||
"name": "domain-property-ownership",
|
||||
"status": "completed",
|
||||
"summary": "Migrated Domain property storage to std::unique_ptr<Property> with shell-property compatibility accessors and cross-reference validation."
|
||||
},
|
||||
{
|
||||
"step": 4,
|
||||
"name": "validation-report-handoff",
|
||||
"status": "completed",
|
||||
"summary": "Validated targeted CTest, workspace validation, script self-tests, and whitespace checks; updated project progress."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
# Step 0: property-base-contract
|
||||
|
||||
## Read First
|
||||
|
||||
Read these files before editing:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PLAN.md`
|
||||
- `/docs/PROGRESS.md`
|
||||
- `/docs/WORKNOTE.md`
|
||||
- `/docs/AGENT_RULES.md`
|
||||
- `/docs/ARCHITECTURE.md`
|
||||
- `/docs/ADR.md`
|
||||
- `/docs/implementation-plans/domain-runtime-storage-implementation-plan.md`
|
||||
- `/include/fesa/property/ShellProperty.hpp`
|
||||
- `/include/fesa/core/Domain.hpp`
|
||||
- `/src/core/Domain.cpp`
|
||||
|
||||
## Task
|
||||
|
||||
Create `/docs/implementation-plans/property-model-foundation-implementation-plan.md`.
|
||||
|
||||
The plan must define the runtime property model contract:
|
||||
|
||||
- `Property` is the base class for element properties and sections.
|
||||
- `PropertyKind` identifies concrete property families; phase 1 supports only `Shell`.
|
||||
- `ShellProperty` derives from `Property`.
|
||||
- `Domain` owns property objects through RAII.
|
||||
- `Domain` validates duplicate property ids and missing material ids.
|
||||
- `Element::propertyId()` remains an id reference; elements do not own property objects.
|
||||
|
||||
Keep out of scope:
|
||||
|
||||
- shell section stiffness;
|
||||
- material constitutive matrices;
|
||||
- shear correction;
|
||||
- element formulation;
|
||||
- assembly, solver, HDF5 output, and reference comparison.
|
||||
|
||||
## Tests To Write First
|
||||
|
||||
- Documentation-only step. No C++ test is required in this step.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
python -m unittest discover -s scripts -p "test_*.py"
|
||||
```
|
||||
|
||||
## Verification Notes
|
||||
|
||||
1. Confirm the plan does not change MITC4 formulation or tolerance policy.
|
||||
2. Confirm the plan keeps solver state out of `Domain`.
|
||||
3. Update `phases/property-model-foundation/index.json` for this step result.
|
||||
@@ -0,0 +1,52 @@
|
||||
# Step 1: property-base-interface
|
||||
|
||||
## Read First
|
||||
|
||||
Read these files before editing:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/AGENT_RULES.md`
|
||||
- `/docs/implementation-plans/property-model-foundation-implementation-plan.md`
|
||||
- `/include/fesa/core/ModelTypes.hpp`
|
||||
- `/include/fesa/property/ShellProperty.hpp`
|
||||
- `/tests/property/shell_property_test.cpp`
|
||||
- `/CMakeLists.txt`
|
||||
|
||||
## Task
|
||||
|
||||
Add the runtime property base interface.
|
||||
|
||||
Required API:
|
||||
|
||||
- File: `/include/fesa/property/Property.hpp`
|
||||
- Namespace: `fesa::property`
|
||||
- `enum class PropertyKind { Shell };`
|
||||
- `class Property`
|
||||
- virtual destructor;
|
||||
- `virtual PropertyId id() const noexcept = 0;`
|
||||
- `virtual PropertyKind kind() const noexcept = 0;`
|
||||
|
||||
Rules:
|
||||
|
||||
- Use `fesa::core::PropertyId`.
|
||||
- Do not add section stiffness or material behavior.
|
||||
- Keep this header independent from HDF5, MKL, TBB, and parser headers.
|
||||
|
||||
## Tests To Write First
|
||||
|
||||
- Add `/tests/property/property_base_test.cpp`.
|
||||
- Test that a small derived class can be used through `const Property&`.
|
||||
- Test virtual deletion through `std::unique_ptr<Property>`.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object
|
||||
python scripts/validate_workspace.py
|
||||
```
|
||||
|
||||
## Verification Notes
|
||||
|
||||
1. Run targeted CTest before implementation and confirm the expected compile failure.
|
||||
2. Keep all code C++17/MSVC-compatible.
|
||||
3. Update `phases/property-model-foundation/index.json` for this step result.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Step 2: shell-property-polymorphism
|
||||
|
||||
## Read First
|
||||
|
||||
Read these files before editing:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/AGENT_RULES.md`
|
||||
- `/docs/implementation-plans/property-model-foundation-implementation-plan.md`
|
||||
- `/include/fesa/property/Property.hpp`
|
||||
- `/include/fesa/property/ShellProperty.hpp`
|
||||
- `/src/property/ShellProperty.cpp`
|
||||
- `/tests/property/shell_property_test.cpp`
|
||||
|
||||
## Task
|
||||
|
||||
Make `ShellProperty` derive from `Property`.
|
||||
|
||||
Required behavior:
|
||||
|
||||
- `ShellProperty::id()` overrides `Property::id()`.
|
||||
- `ShellProperty::kind()` returns `PropertyKind::Shell`.
|
||||
- `ShellProperty` keeps `materialId()` and `thickness()` accessors.
|
||||
- Constructor still rejects `thickness <= 0.0`.
|
||||
|
||||
Rules:
|
||||
|
||||
- Do not rename `ShellProperty` in this phase.
|
||||
- Do not add shell section stiffness.
|
||||
- Do not add material lookup inside `ShellProperty`; Domain validates cross references.
|
||||
|
||||
## Tests To Write First
|
||||
|
||||
- Extend `/tests/property/shell_property_test.cpp`.
|
||||
- Test `const Property& base = shell_property` exposes id and kind.
|
||||
- Preserve positive-thickness validation tests.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R model-object
|
||||
python scripts/validate_workspace.py
|
||||
```
|
||||
|
||||
## Verification Notes
|
||||
|
||||
1. Run targeted CTest before production edits and confirm failure.
|
||||
2. Keep `ShellProperty` free of solver state.
|
||||
3. Update `phases/property-model-foundation/index.json` for this step result.
|
||||
@@ -0,0 +1,60 @@
|
||||
# Step 3: domain-property-ownership
|
||||
|
||||
## Read First
|
||||
|
||||
Read these files before editing:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/AGENT_RULES.md`
|
||||
- `/docs/implementation-plans/property-model-foundation-implementation-plan.md`
|
||||
- `/include/fesa/core/Domain.hpp`
|
||||
- `/src/core/Domain.cpp`
|
||||
- `/include/fesa/property/Property.hpp`
|
||||
- `/include/fesa/property/ShellProperty.hpp`
|
||||
- `/tests/core/domain_storage_test.cpp`
|
||||
- `/tests/core/domain_model_object_test.cpp`
|
||||
|
||||
## Task
|
||||
|
||||
Make `Domain` own runtime property objects through the `Property` base class.
|
||||
|
||||
Required API shape:
|
||||
|
||||
- `void Domain::addProperty(std::unique_ptr<fesa::property::Property> property);`
|
||||
- `const fesa::property::Property* Domain::findProperty(PropertyId id) const noexcept;`
|
||||
- `const fesa::property::Property& Domain::property(PropertyId id) const;`
|
||||
- `std::size_t Domain::propertyCount() const noexcept;`
|
||||
|
||||
Compatibility helpers may remain if useful:
|
||||
|
||||
- `addShellProperty(std::unique_ptr<fesa::property::ShellProperty>)`
|
||||
- `findShellProperty(PropertyId)`
|
||||
- `shellProperty(PropertyId)`
|
||||
- `shellPropertyCount()`
|
||||
|
||||
Rules:
|
||||
|
||||
- Property storage must be `std::unique_ptr<Property>`.
|
||||
- Reject null property pointers.
|
||||
- Reject duplicate property ids.
|
||||
- For `ShellProperty`, reject missing material id.
|
||||
- `Domain::addElement` must validate `Element::propertyId()` using runtime property storage.
|
||||
|
||||
## Tests To Write First
|
||||
|
||||
- Rewrite relevant property assertions in `/tests/core/domain_storage_test.cpp` to use runtime property ownership.
|
||||
- Add a test that `const Domain::property(id)` returns `const Property&`.
|
||||
- Preserve direct `ShellProperty` lookup for now if compatibility helpers remain.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R "domain|model-object"
|
||||
python scripts/validate_workspace.py
|
||||
```
|
||||
|
||||
## Verification Notes
|
||||
|
||||
1. Run targeted CTest before production edits and confirm failure.
|
||||
2. Do not add solver vectors, equation ids, or integration-point state to `Domain`.
|
||||
3. Update `phases/property-model-foundation/index.json` for this step result.
|
||||
@@ -0,0 +1,42 @@
|
||||
# Step 4: validation-report-handoff
|
||||
|
||||
## Read First
|
||||
|
||||
Read these files before editing:
|
||||
|
||||
- `/AGENTS.md`
|
||||
- `/docs/PLAN.md`
|
||||
- `/docs/PROGRESS.md`
|
||||
- `/docs/WORKNOTE.md`
|
||||
- `/docs/AGENT_RULES.md`
|
||||
- `/docs/implementation-plans/property-model-foundation-implementation-plan.md`
|
||||
- `/phases/property-model-foundation/index.json`
|
||||
|
||||
## Task
|
||||
|
||||
Record validation evidence and handoff notes after the property model foundation implementation.
|
||||
|
||||
Required updates:
|
||||
|
||||
- Update `/docs/PROGRESS.md` with completed property base and Domain property ownership work.
|
||||
- Update `/docs/PLAN.md` only if sequencing or acceptance criteria changed.
|
||||
- Update `/docs/WORKNOTE.md` only if there were failures, repeated mistakes, or environment traps.
|
||||
- Mark `/phases/property-model-foundation/index.json` steps and `/phases/index.json` according to actual results.
|
||||
|
||||
## Tests To Write First
|
||||
|
||||
- Documentation/reporting step. No new C++ test is required.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
```powershell
|
||||
python -m unittest discover -s scripts -p "test_*.py"
|
||||
python scripts/validate_workspace.py
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## Verification Notes
|
||||
|
||||
1. Do not claim MITC4 formulation correctness.
|
||||
2. Do not claim HDF5 result output is implemented.
|
||||
3. Keep validation evidence concrete: command, result, and scope.
|
||||
+40
-11
@@ -172,8 +172,8 @@ void Domain::addElement(std::unique_ptr<fesa::element::Element> element) {
|
||||
throw std::invalid_argument("element references missing node id");
|
||||
}
|
||||
}
|
||||
if (findShellProperty(element->propertyId()) == nullptr) {
|
||||
throw std::invalid_argument("element references missing shell property id");
|
||||
if (findProperty(element->propertyId()) == nullptr) {
|
||||
throw std::invalid_argument("element references missing property id");
|
||||
}
|
||||
elements_.emplace(id, std::move(element));
|
||||
}
|
||||
@@ -189,15 +189,28 @@ void Domain::addMaterial(std::unique_ptr<fesa::material::Material> material) {
|
||||
}
|
||||
}
|
||||
|
||||
void Domain::addShellProperty(fesa::property::ShellProperty property) {
|
||||
const PropertyId id = property.id();
|
||||
if (shell_properties_.find(id) != shell_properties_.end()) {
|
||||
throw std::invalid_argument("duplicate shell property id");
|
||||
void Domain::addProperty(std::unique_ptr<fesa::property::Property> property) {
|
||||
if (!property) {
|
||||
throw std::invalid_argument("property is null");
|
||||
}
|
||||
if (findMaterial(property.materialId()) == nullptr) {
|
||||
const PropertyId id = property->id();
|
||||
if (properties_.find(id) != properties_.end()) {
|
||||
throw std::invalid_argument("duplicate property id");
|
||||
}
|
||||
if (property->kind() == fesa::property::PropertyKind::Shell) {
|
||||
const auto* shell_property = dynamic_cast<const fesa::property::ShellProperty*>(property.get());
|
||||
if (shell_property == nullptr) {
|
||||
throw std::invalid_argument("shell property kind does not match shell property type");
|
||||
}
|
||||
if (findMaterial(shell_property->materialId()) == nullptr) {
|
||||
throw std::invalid_argument("shell property references missing material id");
|
||||
}
|
||||
shell_properties_.emplace(id, std::move(property));
|
||||
}
|
||||
properties_.emplace(id, std::move(property));
|
||||
}
|
||||
|
||||
void Domain::addShellProperty(std::unique_ptr<fesa::property::ShellProperty> property) {
|
||||
addProperty(std::move(property));
|
||||
}
|
||||
|
||||
void Domain::addNodeSet(std::string name, std::vector<NodeId> node_ids) {
|
||||
@@ -338,9 +351,25 @@ std::size_t Domain::materialCount() const noexcept {
|
||||
return materials_.size();
|
||||
}
|
||||
|
||||
const fesa::property::Property* Domain::findProperty(PropertyId id) const noexcept {
|
||||
const auto it = properties_.find(id);
|
||||
return it == properties_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
const fesa::property::Property& Domain::property(PropertyId id) const {
|
||||
const fesa::property::Property* found = findProperty(id);
|
||||
if (found == nullptr) {
|
||||
throw std::out_of_range("property id not found");
|
||||
}
|
||||
return *found;
|
||||
}
|
||||
|
||||
std::size_t Domain::propertyCount() const noexcept {
|
||||
return properties_.size();
|
||||
}
|
||||
|
||||
const fesa::property::ShellProperty* Domain::findShellProperty(PropertyId id) const noexcept {
|
||||
const auto it = shell_properties_.find(id);
|
||||
return it == shell_properties_.end() ? nullptr : &it->second;
|
||||
return dynamic_cast<const fesa::property::ShellProperty*>(findProperty(id));
|
||||
}
|
||||
|
||||
const fesa::property::ShellProperty& Domain::shellProperty(PropertyId id) const {
|
||||
@@ -352,7 +381,7 @@ const fesa::property::ShellProperty& Domain::shellProperty(PropertyId id) const
|
||||
}
|
||||
|
||||
std::size_t Domain::shellPropertyCount() const noexcept {
|
||||
return shell_properties_.size();
|
||||
return properties_.size();
|
||||
}
|
||||
|
||||
const std::vector<NodeId>* Domain::findNodeSet(const std::string& name) const noexcept {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#include "fesa/io/Hdf5ResultWriter.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
|
||||
namespace fesa::io {
|
||||
|
||||
Hdf5ResultWriter::Hdf5ResultWriter(std::string file_path)
|
||||
: file_path_(std::move(file_path)) {
|
||||
if (file_path_.empty()) {
|
||||
throw std::invalid_argument("HDF5 result writer path must not be empty");
|
||||
}
|
||||
}
|
||||
|
||||
const std::string& Hdf5ResultWriter::filePath() const noexcept {
|
||||
return file_path_;
|
||||
}
|
||||
|
||||
} // namespace fesa::io
|
||||
@@ -15,6 +15,10 @@ PropertyId ShellProperty::id() const noexcept {
|
||||
return id_;
|
||||
}
|
||||
|
||||
PropertyKind ShellProperty::kind() const noexcept {
|
||||
return PropertyKind::Shell;
|
||||
}
|
||||
|
||||
MaterialId ShellProperty::materialId() const noexcept {
|
||||
return material_id_;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fesa::core::Domain populated_domain() {
|
||||
domain.addNode(fesa::core::Node{3, 1.0, 1.0, 0.0});
|
||||
domain.addNode(fesa::core::Node{4, 0.0, 1.0, 0.0});
|
||||
domain.addMaterial(std::make_unique<fesa::material::LinearElasticMaterial>(700, 210.0, 0.3));
|
||||
domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01});
|
||||
domain.addShellProperty(std::make_unique<fesa::property::ShellProperty>(500, 700, 0.01));
|
||||
return domain;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ void add_four_nodes(fesa::core::Domain& domain) {
|
||||
|
||||
void add_material_and_property(fesa::core::Domain& domain) {
|
||||
domain.addMaterial(std::make_unique<fesa::material::LinearElasticMaterial>(700, 210.0, 0.3));
|
||||
domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01});
|
||||
domain.addShellProperty(std::make_unique<fesa::property::ShellProperty>(500, 700, 0.01));
|
||||
}
|
||||
|
||||
void add_element(fesa::core::Domain& domain) {
|
||||
@@ -215,21 +215,29 @@ int add_and_retrieve_material_and_property() {
|
||||
return result;
|
||||
}
|
||||
|
||||
const fesa::property::ShellProperty* property = domain.findShellProperty(500);
|
||||
const fesa::property::Property* property = domain.findProperty(500);
|
||||
if (const int result = require(property != nullptr); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(property->materialId() == 700); result != 0) {
|
||||
if (const int result = require(property->kind() == fesa::property::PropertyKind::Shell); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(property->thickness() == 0.01); result != 0) {
|
||||
|
||||
const fesa::property::ShellProperty* shell_property = domain.findShellProperty(500);
|
||||
if (const int result = require(shell_property != nullptr); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(shell_property->materialId() == 700); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(shell_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);
|
||||
return require(domain.property(500).id() == 500);
|
||||
}
|
||||
|
||||
int duplicate_material_and_property_ids_throw() {
|
||||
@@ -244,7 +252,7 @@ int duplicate_material_and_property_ids_throw() {
|
||||
}
|
||||
|
||||
return require_throws<std::invalid_argument>([&domain]() {
|
||||
domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.02});
|
||||
domain.addShellProperty(std::make_unique<fesa::property::ShellProperty>(500, 700, 0.02));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -252,7 +260,15 @@ int shell_property_referencing_missing_material_throws() {
|
||||
fesa::core::Domain domain;
|
||||
|
||||
return require_throws<std::invalid_argument>([&domain]() {
|
||||
domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01});
|
||||
domain.addShellProperty(std::make_unique<fesa::property::ShellProperty>(500, 700, 0.01));
|
||||
});
|
||||
}
|
||||
|
||||
int null_property_rejected() {
|
||||
fesa::core::Domain domain;
|
||||
|
||||
return require_throws<std::invalid_argument>([&domain]() {
|
||||
domain.addProperty(nullptr);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -517,6 +533,10 @@ int const_domain_retrieval_returns_const_runtime_model_data() {
|
||||
result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require((std::is_same<decltype(const_domain.property(500)), const fesa::property::Property&>::value));
|
||||
result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require((std::is_same<decltype(const_domain.shellProperty(500)), const fesa::property::ShellProperty&>::value));
|
||||
result != 0) {
|
||||
return result;
|
||||
@@ -567,7 +587,7 @@ int failed_inserts_do_not_mutate_counts() {
|
||||
}
|
||||
|
||||
if (const int result = require_throws<std::invalid_argument>([&domain]() {
|
||||
domain.addShellProperty(fesa::property::ShellProperty{501, 404, 0.01});
|
||||
domain.addShellProperty(std::make_unique<fesa::property::ShellProperty>(501, 404, 0.01));
|
||||
});
|
||||
result != 0) {
|
||||
return result;
|
||||
@@ -642,6 +662,9 @@ int run_domain_storage_tests() {
|
||||
if (const int result = shell_property_referencing_missing_material_throws(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = null_property_rejected(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = add_and_retrieve_sets(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#include "fesa/io/Hdf5ResultWriter.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
namespace {
|
||||
|
||||
int require(bool condition) {
|
||||
return condition ? 0 : 1;
|
||||
}
|
||||
|
||||
template <typename Exception, typename Function>
|
||||
int require_throws(Function&& function) {
|
||||
try {
|
||||
function();
|
||||
} catch (const Exception&) {
|
||||
return 0;
|
||||
} catch (...) {
|
||||
return 1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int construct_writer_preserves_output_path() {
|
||||
const fesa::io::Hdf5ResultWriter writer{"results.h5"};
|
||||
return require(writer.filePath() == "results.h5");
|
||||
}
|
||||
|
||||
int empty_output_path_throws() {
|
||||
return require_throws<std::invalid_argument>([]() {
|
||||
(void)fesa::io::Hdf5ResultWriter{""};
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
if (const int result = construct_writer_preserves_output_path(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = empty_output_path_throws(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ int run_element_base_tests();
|
||||
int run_load_base_tests();
|
||||
int run_material_base_tests();
|
||||
int run_mitc4_element_model_tests();
|
||||
int run_property_base_tests();
|
||||
int run_shell_property_tests();
|
||||
|
||||
int main() {
|
||||
@@ -18,6 +19,9 @@ int main() {
|
||||
if (const int result = run_material_base_tests(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = run_property_base_tests(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = run_shell_property_tests(); result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "fesa/property/Property.hpp"
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
int require(bool condition) {
|
||||
return condition ? 0 : 1;
|
||||
}
|
||||
|
||||
class TestProperty final : public fesa::property::Property {
|
||||
public:
|
||||
explicit TestProperty(fesa::core::PropertyId id) : id_(id) {}
|
||||
|
||||
fesa::core::PropertyId id() const noexcept override {
|
||||
return id_;
|
||||
}
|
||||
|
||||
fesa::property::PropertyKind kind() const noexcept override {
|
||||
return fesa::property::PropertyKind::Shell;
|
||||
}
|
||||
|
||||
private:
|
||||
fesa::core::PropertyId id_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int run_property_base_tests() {
|
||||
std::unique_ptr<fesa::property::Property> owned = std::make_unique<TestProperty>(500);
|
||||
const fesa::property::Property& property = *owned;
|
||||
|
||||
if (const int result = require(property.id() == 500); result != 0) {
|
||||
return result;
|
||||
}
|
||||
return require(property.kind() == fesa::property::PropertyKind::Shell);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "fesa/property/ShellProperty.hpp"
|
||||
#include "fesa/property/Property.hpp"
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -24,10 +25,17 @@ int require_throws(Function&& function) {
|
||||
|
||||
int run_shell_property_tests() {
|
||||
const fesa::property::ShellProperty property{500, 700, 0.01};
|
||||
const fesa::property::Property& base = property;
|
||||
|
||||
if (const int result = require(property.id() == 500); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(base.id() == 500); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(base.kind() == fesa::property::PropertyKind::Shell); result != 0) {
|
||||
return result;
|
||||
}
|
||||
if (const int result = require(property.materialId() == 700); result != 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user