diff --git a/CMakeLists.txt b/CMakeLists.txt index f057e31..75f913b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 744cff9..b79daac 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -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`. +- 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. diff --git a/docs/implementation-plans/property-model-foundation-implementation-plan.md b/docs/implementation-plans/property-model-foundation-implementation-plan.md new file mode 100644 index 0000000..3b71136 --- /dev/null +++ b/docs/implementation-plans/property-model-foundation-implementation-plan.md @@ -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`. + - 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> +``` + +`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 +``` diff --git a/include/fesa/core/Domain.hpp b/include/fesa/core/Domain.hpp index fb7ad1f..a829047 100644 --- a/include/fesa/core/Domain.hpp +++ b/include/fesa/core/Domain.hpp @@ -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 @@ -22,7 +23,8 @@ public: void addNode(Node node); void addElement(std::unique_ptr element); void addMaterial(std::unique_ptr material); - void addShellProperty(fesa::property::ShellProperty property); + void addProperty(std::unique_ptr property); + void addShellProperty(std::unique_ptr property); void addNodeSet(std::string name, std::vector node_ids); void addElementSet(std::string name, std::vector element_ids); std::size_t addBoundaryCondition(std::unique_ptr 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 nodes_; std::unordered_map> elements_; std::unordered_map> materials_; - std::unordered_map shell_properties_; + std::unordered_map> properties_; std::unordered_map> node_sets_; std::unordered_map> element_sets_; std::vector> boundary_conditions_; diff --git a/include/fesa/io/Hdf5ResultWriter.hpp b/include/fesa/io/Hdf5ResultWriter.hpp new file mode 100644 index 0000000..3ab1b1b --- /dev/null +++ b/include/fesa/io/Hdf5ResultWriter.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +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 diff --git a/include/fesa/property/Property.hpp b/include/fesa/property/Property.hpp new file mode 100644 index 0000000..1ece899 --- /dev/null +++ b/include/fesa/property/Property.hpp @@ -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 diff --git a/include/fesa/property/ShellProperty.hpp b/include/fesa/property/ShellProperty.hpp index 0743ce1..4c7b598 100644 --- a/include/fesa/property/ShellProperty.hpp +++ b/include/fesa/property/ShellProperty.hpp @@ -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; diff --git a/phases/index.json b/phases/index.json index 3165eff..d7b9606 100644 --- a/phases/index.json +++ b/phases/index.json @@ -11,6 +11,10 @@ { "dir": "domain-runtime-storage", "status": "completed" + }, + { + "dir": "property-model-foundation", + "status": "completed" } ] } diff --git a/phases/property-model-foundation/index.json b/phases/property-model-foundation/index.json new file mode 100644 index 0000000..b81cc59 --- /dev/null +++ b/phases/property-model-foundation/index.json @@ -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 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." + } + ] +} diff --git a/phases/property-model-foundation/step0.md b/phases/property-model-foundation/step0.md new file mode 100644 index 0000000..144911a --- /dev/null +++ b/phases/property-model-foundation/step0.md @@ -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. diff --git a/phases/property-model-foundation/step1.md b/phases/property-model-foundation/step1.md new file mode 100644 index 0000000..173d8da --- /dev/null +++ b/phases/property-model-foundation/step1.md @@ -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`. + +## 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. diff --git a/phases/property-model-foundation/step2.md b/phases/property-model-foundation/step2.md new file mode 100644 index 0000000..553bda4 --- /dev/null +++ b/phases/property-model-foundation/step2.md @@ -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. diff --git a/phases/property-model-foundation/step3.md b/phases/property-model-foundation/step3.md new file mode 100644 index 0000000..a1479d7 --- /dev/null +++ b/phases/property-model-foundation/step3.md @@ -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 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)` +- `findShellProperty(PropertyId)` +- `shellProperty(PropertyId)` +- `shellPropertyCount()` + +Rules: + +- Property storage must be `std::unique_ptr`. +- 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. diff --git a/phases/property-model-foundation/step4.md b/phases/property-model-foundation/step4.md new file mode 100644 index 0000000..e1b527d --- /dev/null +++ b/phases/property-model-foundation/step4.md @@ -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. diff --git a/src/core/Domain.cpp b/src/core/Domain.cpp index f5ab39d..fad9d59 100644 --- a/src/core/Domain.cpp +++ b/src/core/Domain.cpp @@ -172,8 +172,8 @@ void Domain::addElement(std::unique_ptr 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 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 property) { + if (!property) { + throw std::invalid_argument("property is null"); } - if (findMaterial(property.materialId()) == nullptr) { - throw std::invalid_argument("shell property references missing material id"); + const PropertyId id = property->id(); + if (properties_.find(id) != properties_.end()) { + throw std::invalid_argument("duplicate property id"); } - shell_properties_.emplace(id, std::move(property)); + if (property->kind() == fesa::property::PropertyKind::Shell) { + const auto* shell_property = dynamic_cast(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"); + } + } + properties_.emplace(id, std::move(property)); +} + +void Domain::addShellProperty(std::unique_ptr property) { + addProperty(std::move(property)); } void Domain::addNodeSet(std::string name, std::vector 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(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* Domain::findNodeSet(const std::string& name) const noexcept { diff --git a/src/io/Hdf5ResultWriter.cpp b/src/io/Hdf5ResultWriter.cpp new file mode 100644 index 0000000..c03c4d9 --- /dev/null +++ b/src/io/Hdf5ResultWriter.cpp @@ -0,0 +1,19 @@ +#include "fesa/io/Hdf5ResultWriter.hpp" + +#include +#include + +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 diff --git a/src/property/ShellProperty.cpp b/src/property/ShellProperty.cpp index 2b2104c..59a88fe 100644 --- a/src/property/ShellProperty.cpp +++ b/src/property/ShellProperty.cpp @@ -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_; } diff --git a/tests/core/domain_model_object_test.cpp b/tests/core/domain_model_object_test.cpp index 6482545..187510f 100644 --- a/tests/core/domain_model_object_test.cpp +++ b/tests/core/domain_model_object_test.cpp @@ -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(700, 210.0, 0.3)); - domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01}); + domain.addShellProperty(std::make_unique(500, 700, 0.01)); return domain; } diff --git a/tests/core/domain_storage_test.cpp b/tests/core/domain_storage_test.cpp index e11956a..8d57036 100644 --- a/tests/core/domain_storage_test.cpp +++ b/tests/core/domain_storage_test.cpp @@ -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(700, 210.0, 0.3)); - domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01}); + domain.addShellProperty(std::make_unique(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([&domain]() { - domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.02}); + domain.addShellProperty(std::make_unique(500, 700, 0.02)); }); } @@ -252,7 +260,15 @@ int shell_property_referencing_missing_material_throws() { fesa::core::Domain domain; return require_throws([&domain]() { - domain.addShellProperty(fesa::property::ShellProperty{500, 700, 0.01}); + domain.addShellProperty(std::make_unique(500, 700, 0.01)); + }); +} + +int null_property_rejected() { + fesa::core::Domain domain; + + return require_throws([&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::value)); + result != 0) { + return result; + } if (const int result = require((std::is_same::value)); result != 0) { return result; @@ -567,7 +587,7 @@ int failed_inserts_do_not_mutate_counts() { } if (const int result = require_throws([&domain]() { - domain.addShellProperty(fesa::property::ShellProperty{501, 404, 0.01}); + domain.addShellProperty(std::make_unique(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; } diff --git a/tests/io/hdf5_result_writer_test.cpp b/tests/io/hdf5_result_writer_test.cpp new file mode 100644 index 0000000..b951daa --- /dev/null +++ b/tests/io/hdf5_result_writer_test.cpp @@ -0,0 +1,44 @@ +#include "fesa/io/Hdf5ResultWriter.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; +} + +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([]() { + (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; +} diff --git a/tests/model_object_main.cpp b/tests/model_object_main.cpp index 277e843..54d64f2 100644 --- a/tests/model_object_main.cpp +++ b/tests/model_object_main.cpp @@ -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; } diff --git a/tests/property/property_base_test.cpp b/tests/property/property_base_test.cpp new file mode 100644 index 0000000..a0d478f --- /dev/null +++ b/tests/property/property_base_test.cpp @@ -0,0 +1,37 @@ +#include "fesa/property/Property.hpp" + +#include + +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 owned = std::make_unique(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); +} diff --git a/tests/property/shell_property_test.cpp b/tests/property/shell_property_test.cpp index 3d2f248..de779cb 100644 --- a/tests/property/shell_property_test.cpp +++ b/tests/property/shell_property_test.cpp @@ -1,4 +1,5 @@ #include "fesa/property/ShellProperty.hpp" +#include "fesa/property/Property.hpp" #include @@ -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; }