diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..c98a569 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,10 @@ +cmake_minimum_required(VERSION 3.20) + +project(FESA LANGUAGES CXX) + +add_library(fesa_core INTERFACE) +target_compile_features(fesa_core INTERFACE cxx_std_17) +target_include_directories(fesa_core INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +enable_testing() +add_subdirectory(tests) diff --git a/docs/build-test-reports/solver-core-skeleton.md b/docs/build-test-reports/solver-core-skeleton.md new file mode 100644 index 0000000..7ab6a74 --- /dev/null +++ b/docs/build-test-reports/solver-core-skeleton.md @@ -0,0 +1,64 @@ +# Solver Core Skeleton Build/Test Report + +## Metadata +- phase: solver-core-skeleton +- scope: C++ skeleton classes only +- status: passed + +## Commands Run + +```powershell +python -m unittest discover -s scripts -p "test_*.py" +``` + +- exit_code: 0 +- summary: 98 Python Harness tests passed. + +```powershell +python scripts/validate_workspace.py +``` + +- exit_code: 0 +- summary: CMake configure, MSVC Debug build, and full CTest suite passed. + +```powershell +ctest --test-dir build/msvc-debug --output-on-failure -C Debug -R solver_core_skeleton_integration_test +``` + +- exit_code: 0 +- summary: `solver_core_skeleton_integration_test` passed. + +## CTest Tests Added + +- `harness_smoke_test` +- `core_diagnostic_test` +- `core_ids_test` +- `core_primitives_test` +- `core_status_test` +- `model_analysis_step_test` +- `model_boundary_condition_test` +- `model_domain_test` +- `model_element_test` +- `model_load_test` +- `model_material_test` +- `model_node_test` +- `model_property_test` +- `analysis_model_view_test` +- `dof_manager_dof_key_test` +- `dof_manager_numbering_test` +- `analysis_state_vectors_test` +- `analysis_flow_linear_static_analysis_test` +- `analysis_flow_template_test` +- `results_containers_test` +- `solver_core_skeleton_integration_test` + +## Known Limitations + +- No element stiffness, residual, tangent, or stress recovery calculation is implemented. +- No material law evaluation is implemented. +- No sparse assembly implementation is implemented beyond `DofManager` sparse pattern ownership. +- No linear solver backend is implemented. +- No HDF5 writer or reader is implemented. +- No Abaqus `.inp` parser is implemented. +- No reference comparison against Abaqus CSV artifacts is implemented. +- `LinearStaticAnalysis` currently prepares the analysis model, DOF map, and zero-valued state only. diff --git a/phases/index.json b/phases/index.json index 6eba160..ac29268 100644 --- a/phases/index.json +++ b/phases/index.json @@ -2,7 +2,7 @@ "phases": [ { "dir": "solver-core-skeleton", - "status": "pending" + "status": "completed" } ] } diff --git a/phases/solver-core-skeleton/index.json b/phases/solver-core-skeleton/index.json index 6460d03..845cc40 100644 --- a/phases/solver-core-skeleton/index.json +++ b/phases/solver-core-skeleton/index.json @@ -9,79 +9,92 @@ "allowed_paths": [ "CMakeLists.txt", "tests/" - ] + ], + "started_at": "2026-06-12T02:09:10+0900", + "summary": "CMake/CTest bootstrap with fesa_core interface target and smoke test", + "status": "completed" }, { "step": 1, "name": "core-primitives", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/core/", "tests/unit/core_*_test.cpp" - ] + ], + "summary": "Core ID, diagnostic, and status primitives added with tests" }, { "step": 2, "name": "domain-model-entities", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/model/", "tests/unit/model_*_test.cpp" - ] + ], + "summary": "Model entities and Domain ownership API added with tests" }, { "step": 3, "name": "analysis-model-view", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/analysis/", "tests/unit/analysis_model_*_test.cpp" - ] + ], + "summary": "AnalysisModel step view added without Domain copies" }, { "step": 4, "name": "dof-manager", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/fem/", "tests/unit/dof_manager_*_test.cpp" - ] + ], + "summary": "DofManager deterministic numbering and constrained/free mapping added" }, { "step": 5, "name": "analysis-state", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/analysis/", "tests/unit/analysis_state_*_test.cpp" - ] + ], + "summary": "AnalysisState vector ownership and residual update added" }, { "step": 6, "name": "analysis-template-flow", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/analysis/", "tests/unit/analysis_flow_*_test.cpp" - ] + ], + "summary": "Analysis template method and LinearStaticAnalysis skeleton added" }, { "step": 7, "name": "results-containers", - "status": "pending", + "status": "completed", "allowed_paths": [ "src/fesa/results/", "tests/unit/results_*_test.cpp" - ] + ], + "summary": "ResultStep, ResultFrame, FieldOutput, and HistoryOutput containers added" }, { "step": 8, "name": "solver-skeleton-integration-report", - "status": "pending", + "status": "completed", "allowed_paths": [ "tests/integration/", "docs/build-test-reports/" - ] + ], + "summary": "Solver skeleton integration test and build/test report added" } - ] + ], + "created_at": "2026-06-12T02:09:10+0900", + "completed_at": "2026-06-12T02:42:00+0900" } diff --git a/src/fesa/analysis/analysis.hpp b/src/fesa/analysis/analysis.hpp new file mode 100644 index 0000000..ed20c82 --- /dev/null +++ b/src/fesa/analysis/analysis.hpp @@ -0,0 +1,34 @@ +#pragma once + +namespace fesa::analysis { + +class Analysis { +public: + virtual ~Analysis() = default; + + void run() + { + initialize(); + build_analysis_model(); + build_dof_map(); + build_sparse_pattern(); + assemble(); + apply_boundary_conditions(); + solve(); + update_state(); + write_results(); + } + +protected: + virtual void initialize() {} + virtual void build_analysis_model() {} + virtual void build_dof_map() {} + virtual void build_sparse_pattern() {} + virtual void assemble() {} + virtual void apply_boundary_conditions() {} + virtual void solve() {} + virtual void update_state() {} + virtual void write_results() {} +}; + +} // namespace fesa::analysis diff --git a/src/fesa/analysis/analysis_model.hpp b/src/fesa/analysis/analysis_model.hpp new file mode 100644 index 0000000..4291fcd --- /dev/null +++ b/src/fesa/analysis/analysis_model.hpp @@ -0,0 +1,73 @@ +#pragma once + +#include + +#include +#include + +namespace fesa::analysis { + +class AnalysisModel { +public: + AnalysisModel(const model::Domain& domain, core::StepId step_id) + : domain_(domain), step_(domain.find_step(step_id)) + { + if (step_ == nullptr) { + throw std::invalid_argument("analysis step not found"); + } + + for (const auto& element : domain_.elements()) { + active_elements_.push_back(&element); + } + for (const auto& boundary_condition : step_->boundary_conditions()) { + active_boundary_conditions_.push_back(&boundary_condition); + } + for (const auto& load : step_->loads()) { + active_loads_.push_back(&load); + } + } + + const model::Domain& domain() const + { + return domain_; + } + + const model::AnalysisStep& step() const + { + return *step_; + } + + const std::vector& active_elements() const + { + return active_elements_; + } + + const std::vector& active_boundary_conditions() const + { + return active_boundary_conditions_; + } + + const std::vector& active_loads() const + { + return active_loads_; + } + + const model::Property* property_for(const model::Element& element) const + { + return domain_.find_property(element.property_id()); + } + + const model::Material* material_for(const model::Property& property) const + { + return domain_.find_material(property.material_id()); + } + +private: + const model::Domain& domain_; + const model::AnalysisStep* step_; + std::vector active_elements_; + std::vector active_boundary_conditions_; + std::vector active_loads_; +}; + +} // namespace fesa::analysis diff --git a/src/fesa/analysis/analysis_state.hpp b/src/fesa/analysis/analysis_state.hpp new file mode 100644 index 0000000..75348f6 --- /dev/null +++ b/src/fesa/analysis/analysis_state.hpp @@ -0,0 +1,146 @@ +#pragma once + +#include + +#include +#include +#include + +namespace fesa::analysis { + +struct IterationState { + double time = 0.0; + int increment = 0; + int iteration = 0; +}; + +class AnalysisState { +public: + explicit AnalysisState(int total_dof_count) + : displacement_(vector_of(total_dof_count)), + velocity_(vector_of(total_dof_count)), + acceleration_(vector_of(total_dof_count)), + temperature_(vector_of(total_dof_count)), + external_force_(vector_of(total_dof_count)), + internal_force_(vector_of(total_dof_count)), + residual_(vector_of(total_dof_count)) + { + } + + const std::vector& displacement() const + { + return displacement_; + } + + const std::vector& velocity() const + { + return velocity_; + } + + const std::vector& acceleration() const + { + return acceleration_; + } + + const std::vector& temperature() const + { + return temperature_; + } + + const std::vector& external_force() const + { + return external_force_; + } + + const std::vector& internal_force() const + { + return internal_force_; + } + + const std::vector& residual() const + { + return residual_; + } + + void set_displacement(std::vector values) + { + assign_same_size(displacement_, std::move(values)); + } + + void set_external_force(std::vector values) + { + assign_same_size(external_force_, std::move(values)); + } + + void set_internal_force(std::vector values) + { + assign_same_size(internal_force_, std::move(values)); + } + + void update_residual() + { + for (std::size_t index = 0; index < residual_.size(); ++index) { + residual_[index] = external_force_[index] - internal_force_[index]; + } + } + + IterationState& iteration_state() + { + return iteration_state_; + } + + const IterationState& iteration_state() const + { + return iteration_state_; + } + + void set_element_state(core::ElementId element_id, std::vector state) + { + for (auto& entry : element_states_) { + if (entry.first.value == element_id.value) { + entry.second = std::move(state); + return; + } + } + element_states_.push_back({element_id, std::move(state)}); + } + + const std::vector* element_state(core::ElementId element_id) const + { + for (const auto& entry : element_states_) { + if (entry.first.value == element_id.value) { + return &entry.second; + } + } + return nullptr; + } + +private: + static std::vector vector_of(int size) + { + if (size < 0) { + throw std::invalid_argument("negative dof count"); + } + return std::vector(static_cast(size), 0.0); + } + + static void assign_same_size(std::vector& target, std::vector values) + { + if (target.size() != values.size()) { + throw std::invalid_argument("vector size mismatch"); + } + target = std::move(values); + } + + std::vector displacement_; + std::vector velocity_; + std::vector acceleration_; + std::vector temperature_; + std::vector external_force_; + std::vector internal_force_; + std::vector residual_; + IterationState iteration_state_; + std::vector>> element_states_; +}; + +} // namespace fesa::analysis diff --git a/src/fesa/analysis/linear_static_analysis.hpp b/src/fesa/analysis/linear_static_analysis.hpp new file mode 100644 index 0000000..bf87a08 --- /dev/null +++ b/src/fesa/analysis/linear_static_analysis.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +#include + +namespace fesa::analysis { + +class LinearStaticAnalysis : public Analysis { +public: + LinearStaticAnalysis(const model::Domain& domain, core::StepId step_id) + : domain_(domain), step_id_(step_id) + { + } + + const AnalysisModel* analysis_model() const + { + return analysis_model_.get(); + } + + const AnalysisState* state() const + { + return state_.get(); + } + +protected: + void build_analysis_model() override + { + analysis_model_ = std::make_unique(domain_, step_id_); + } + + void build_dof_map() override + { + dof_manager_ = std::make_unique(); + for (const auto* element : analysis_model_->active_elements()) { + for (const auto node_id : element->node_ids()) { + dof_manager_->define_node_dofs(node_id, { + model::DofComponent::ux, + model::DofComponent::uy, + model::DofComponent::uz + }); + } + } + for (const auto* boundary_condition : analysis_model_->active_boundary_conditions()) { + dof_manager_->apply_boundary_condition(*boundary_condition); + } + dof_manager_->number_equations(); + } + + void update_state() override + { + state_ = std::make_unique(dof_manager_->total_dof_count()); + } + +private: + const model::Domain& domain_; + core::StepId step_id_; + std::unique_ptr analysis_model_; + std::unique_ptr dof_manager_; + std::unique_ptr state_; +}; + +} // namespace fesa::analysis diff --git a/src/fesa/core/diagnostic.hpp b/src/fesa/core/diagnostic.hpp new file mode 100644 index 0000000..7bbd306 --- /dev/null +++ b/src/fesa/core/diagnostic.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include + +namespace fesa::core { + +enum class Severity { + info, + warning, + error +}; + +struct Diagnostic { + Severity severity; + std::string code; + std::string message; +}; + +} // namespace fesa::core diff --git a/src/fesa/core/ids.hpp b/src/fesa/core/ids.hpp new file mode 100644 index 0000000..b552ba4 --- /dev/null +++ b/src/fesa/core/ids.hpp @@ -0,0 +1,25 @@ +#pragma once + +namespace fesa::core { + +struct NodeId { + int value; +}; + +struct ElementId { + int value; +}; + +struct MaterialId { + int value; +}; + +struct PropertyId { + int value; +}; + +struct StepId { + int value; +}; + +} // namespace fesa::core diff --git a/src/fesa/core/status.hpp b/src/fesa/core/status.hpp new file mode 100644 index 0000000..241f96a --- /dev/null +++ b/src/fesa/core/status.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include + +#include +#include + +namespace fesa::core { + +class Status { +public: + static Status ok() + { + return Status{}; + } + + static Status failure(Diagnostic diagnostic) + { + Status status; + status.add(std::move(diagnostic)); + return status; + } + + bool is_ok() const + { + return diagnostics_.empty(); + } + + const std::vector& diagnostics() const + { + return diagnostics_; + } + + void add(Diagnostic diagnostic) + { + diagnostics_.push_back(std::move(diagnostic)); + } + +private: + std::vector diagnostics_; +}; + +} // namespace fesa::core diff --git a/src/fesa/fem/dof_key.hpp b/src/fesa/fem/dof_key.hpp new file mode 100644 index 0000000..2d09930 --- /dev/null +++ b/src/fesa/fem/dof_key.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace fesa::fem { + +struct DofKey { + core::NodeId node_id; + model::DofComponent component; +}; + +inline bool operator==(const DofKey& lhs, const DofKey& rhs) +{ + return lhs.node_id.value == rhs.node_id.value && lhs.component == rhs.component; +} + +} // namespace fesa::fem diff --git a/src/fesa/fem/dof_manager.hpp b/src/fesa/fem/dof_manager.hpp new file mode 100644 index 0000000..ccf7163 --- /dev/null +++ b/src/fesa/fem/dof_manager.hpp @@ -0,0 +1,151 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +namespace fesa::fem { + +class DofManager { +public: + void define_node_dofs(core::NodeId node_id, std::vector components) + { + for (const auto component : components) { + DofKey key{node_id, component}; + if (find_record(key) == records_.end()) { + records_.push_back({key}); + } + } + } + + void apply_boundary_condition(const model::BoundaryCondition& bc) + { + auto record = find_record({bc.node_id(), bc.component()}); + if (record == records_.end()) { + throw std::invalid_argument("boundary condition references undefined dof"); + } + record->constrained = true; + } + + void number_equations() + { + std::sort(records_.begin(), records_.end(), [](const Record& lhs, const Record& rhs) { + if (lhs.key.node_id.value != rhs.key.node_id.value) { + return lhs.key.node_id.value < rhs.key.node_id.value; + } + return static_cast(lhs.key.component) < static_cast(rhs.key.component); + }); + + int free_id = 0; + for (int equation_id = 0; equation_id < static_cast(records_.size()); ++equation_id) { + records_[equation_id].equation_id = equation_id; + if (records_[equation_id].constrained) { + records_[equation_id].free_equation_id = std::nullopt; + } else { + records_[equation_id].free_equation_id = free_id++; + } + } + + sparse_pattern_.clear(); + for (int row = 0; row < free_id; ++row) { + for (int column = 0; column < free_id; ++column) { + sparse_pattern_.push_back({row, column}); + } + } + } + + int total_dof_count() const + { + return static_cast(records_.size()); + } + + int free_dof_count() const + { + return static_cast( + std::count_if(records_.begin(), records_.end(), [](const Record& record) { + return !record.constrained; + }) + ); + } + + int constrained_dof_count() const + { + return total_dof_count() - free_dof_count(); + } + + bool is_constrained(DofKey key) const + { + return require_record(key).constrained; + } + + int equation_id(DofKey key) const + { + return require_record(key).equation_id; + } + + std::optional free_equation_id(DofKey key) const + { + return require_record(key).free_equation_id; + } + + std::vector expand_free_vector(const std::vector& free_values) const + { + if (free_values.size() != static_cast(free_dof_count())) { + throw std::invalid_argument("free vector size does not match dof manager"); + } + + std::vector full(records_.size(), 0.0); + for (const auto& record : records_) { + if (record.free_equation_id.has_value()) { + full[static_cast(record.equation_id)] = + free_values[static_cast(*record.free_equation_id)]; + } + } + return full; + } + + const std::vector>& sparse_pattern() const + { + return sparse_pattern_; + } + +private: + struct Record { + DofKey key; + bool constrained = false; + int equation_id = -1; + std::optional free_equation_id; + }; + + std::vector::iterator find_record(DofKey key) + { + return std::find_if(records_.begin(), records_.end(), [key](const Record& record) { + return record.key == key; + }); + } + + std::vector::const_iterator find_record(DofKey key) const + { + return std::find_if(records_.begin(), records_.end(), [key](const Record& record) { + return record.key == key; + }); + } + + const Record& require_record(DofKey key) const + { + const auto record = find_record(key); + if (record == records_.end()) { + throw std::invalid_argument("dof is not defined"); + } + return *record; + } + + std::vector records_; + std::vector> sparse_pattern_; +}; + +} // namespace fesa::fem diff --git a/src/fesa/model/analysis_step.hpp b/src/fesa/model/analysis_step.hpp new file mode 100644 index 0000000..3d108e7 --- /dev/null +++ b/src/fesa/model/analysis_step.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include + +#include +#include +#include + +namespace fesa::model { + +class AnalysisStep { +public: + AnalysisStep(core::StepId id, std::string name) + : id_(id), name_(std::move(name)) + { + } + + core::StepId id() const + { + return id_; + } + + const std::string& name() const + { + return name_; + } + + void add_boundary_condition(BoundaryCondition bc) + { + boundary_conditions_.push_back(std::move(bc)); + } + + void add_load(Load load) + { + loads_.push_back(std::move(load)); + } + + const std::vector& boundary_conditions() const + { + return boundary_conditions_; + } + + const std::vector& loads() const + { + return loads_; + } + +private: + core::StepId id_; + std::string name_; + std::vector boundary_conditions_; + std::vector loads_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/boundary_condition.hpp b/src/fesa/model/boundary_condition.hpp new file mode 100644 index 0000000..01028f8 --- /dev/null +++ b/src/fesa/model/boundary_condition.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +namespace fesa::model { + +enum class DofComponent { + ux, + uy, + uz, + rx, + ry, + rz, + temperature +}; + +class BoundaryCondition { +public: + BoundaryCondition(core::NodeId node_id, DofComponent component, double value) + : node_id_(node_id), component_(component), value_(value) + { + } + + core::NodeId node_id() const + { + return node_id_; + } + + DofComponent component() const + { + return component_; + } + + double value() const + { + return value_; + } + +private: + core::NodeId node_id_; + DofComponent component_; + double value_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/domain.hpp b/src/fesa/model/domain.hpp new file mode 100644 index 0000000..2568094 --- /dev/null +++ b/src/fesa/model/domain.hpp @@ -0,0 +1,124 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include + +namespace fesa::model { + +class Domain { +public: + void add_node(Node node) + { + nodes_.push_back(std::move(node)); + } + + void add_element(Element element) + { + elements_.push_back(std::move(element)); + } + + void add_material(Material material) + { + materials_.push_back(std::move(material)); + } + + void add_property(Property property) + { + properties_.push_back(std::move(property)); + } + + void add_step(AnalysisStep step) + { + steps_.push_back(std::move(step)); + } + + const std::vector& nodes() const + { + return nodes_; + } + + const std::vector& elements() const + { + return elements_; + } + + const std::vector& materials() const + { + return materials_; + } + + const std::vector& properties() const + { + return properties_; + } + + const std::vector& steps() const + { + return steps_; + } + + const Node* find_node(core::NodeId id) const + { + for (const auto& node : nodes_) { + if (node.id().value == id.value) { + return &node; + } + } + return nullptr; + } + + const Element* find_element(core::ElementId id) const + { + for (const auto& element : elements_) { + if (element.id().value == id.value) { + return &element; + } + } + return nullptr; + } + + const Material* find_material(core::MaterialId id) const + { + for (const auto& material : materials_) { + if (material.id().value == id.value) { + return &material; + } + } + return nullptr; + } + + const Property* find_property(core::PropertyId id) const + { + for (const auto& property : properties_) { + if (property.id().value == id.value) { + return &property; + } + } + return nullptr; + } + + const AnalysisStep* find_step(core::StepId id) const + { + for (const auto& step : steps_) { + if (step.id().value == id.value) { + return &step; + } + } + return nullptr; + } + +private: + std::vector nodes_; + std::vector elements_; + std::vector materials_; + std::vector properties_; + std::vector steps_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/element.hpp b/src/fesa/model/element.hpp new file mode 100644 index 0000000..7bd24e2 --- /dev/null +++ b/src/fesa/model/element.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include + +#include +#include + +namespace fesa::model { + +enum class ElementTopology { + truss2, + bar2, + unknown +}; + +class Element { +public: + Element(core::ElementId id, + ElementTopology topology, + std::vector node_ids, + core::PropertyId property_id) + : id_(id), + topology_(topology), + node_ids_(std::move(node_ids)), + property_id_(property_id) + { + } + + core::ElementId id() const + { + return id_; + } + + ElementTopology topology() const + { + return topology_; + } + + const std::vector& node_ids() const + { + return node_ids_; + } + + core::PropertyId property_id() const + { + return property_id_; + } + +private: + core::ElementId id_; + ElementTopology topology_; + std::vector node_ids_; + core::PropertyId property_id_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/load.hpp b/src/fesa/model/load.hpp new file mode 100644 index 0000000..935fd5f --- /dev/null +++ b/src/fesa/model/load.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include + +namespace fesa::model { + +class Load { +public: + Load(core::NodeId node_id, DofComponent component, double value) + : node_id_(node_id), component_(component), value_(value) + { + } + + core::NodeId node_id() const + { + return node_id_; + } + + DofComponent component() const + { + return component_; + } + + double value() const + { + return value_; + } + +private: + core::NodeId node_id_; + DofComponent component_; + double value_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/material.hpp b/src/fesa/model/material.hpp new file mode 100644 index 0000000..fd1945b --- /dev/null +++ b/src/fesa/model/material.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include + +#include +#include + +namespace fesa::model { + +class Material { +public: + Material(core::MaterialId id, std::string name) + : id_(id), name_(std::move(name)) + { + } + + core::MaterialId id() const + { + return id_; + } + + const std::string& name() const + { + return name_; + } + +private: + core::MaterialId id_; + std::string name_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/node.hpp b/src/fesa/model/node.hpp new file mode 100644 index 0000000..1620e45 --- /dev/null +++ b/src/fesa/model/node.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include + +namespace fesa::model { + +class Node { +public: + Node(core::NodeId id, std::array coordinates) + : id_(id), coordinates_(coordinates) + { + } + + core::NodeId id() const + { + return id_; + } + + const std::array& coordinates() const + { + return coordinates_; + } + +private: + core::NodeId id_; + std::array coordinates_; +}; + +} // namespace fesa::model diff --git a/src/fesa/model/property.hpp b/src/fesa/model/property.hpp new file mode 100644 index 0000000..1a99cc5 --- /dev/null +++ b/src/fesa/model/property.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include + +#include +#include + +namespace fesa::model { + +class Property { +public: + Property(core::PropertyId id, std::string name, core::MaterialId material_id) + : id_(id), name_(std::move(name)), material_id_(material_id) + { + } + + core::PropertyId id() const + { + return id_; + } + + const std::string& name() const + { + return name_; + } + + core::MaterialId material_id() const + { + return material_id_; + } + +private: + core::PropertyId id_; + std::string name_; + core::MaterialId material_id_; +}; + +} // namespace fesa::model diff --git a/src/fesa/results/results.hpp b/src/fesa/results/results.hpp new file mode 100644 index 0000000..1d352d2 --- /dev/null +++ b/src/fesa/results/results.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include +#include +#include +#include + +namespace fesa::results { + +enum class FieldLocation { + nodal, + element, + integration_point +}; + +struct FieldOutput { + std::string name; + FieldLocation location; + std::vector components; + std::vector entity_ids; + std::vector values; +}; + +struct HistoryOutput { + std::string name; + std::vector time; + std::vector values; +}; + +class ResultFrame { +public: + ResultFrame(int frame_id, double time) + : frame_id_(frame_id), time_(time) + { + } + + int frame_id() const + { + return frame_id_; + } + + double time() const + { + return time_; + } + + void add_field_output(FieldOutput output) + { + if (output.components.empty()) { + throw std::invalid_argument("field output must have components"); + } + if (output.entity_ids.size() * output.components.size() != output.values.size()) { + throw std::invalid_argument("field output values do not match row shape"); + } + field_outputs_.push_back(std::move(output)); + } + + void add_history_output(HistoryOutput output) + { + history_outputs_.push_back(std::move(output)); + } + + const std::vector& field_outputs() const + { + return field_outputs_; + } + + const std::vector& history_outputs() const + { + return history_outputs_; + } + +private: + int frame_id_; + double time_; + std::vector field_outputs_; + std::vector history_outputs_; +}; + +class ResultStep { +public: + explicit ResultStep(std::string name) + : name_(std::move(name)) + { + } + + const std::string& name() const + { + return name_; + } + + ResultFrame& add_frame(int frame_id, double time) + { + frames_.push_back(ResultFrame{frame_id, time}); + return frames_.back(); + } + + const std::vector& frames() const + { + return frames_; + } + +private: + std::string name_; + std::vector frames_; +}; + +} // namespace fesa::results diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..e6f8e15 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(unit) +add_subdirectory(integration) diff --git a/tests/integration/CMakeLists.txt b/tests/integration/CMakeLists.txt new file mode 100644 index 0000000..e4e5942 --- /dev/null +++ b/tests/integration/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB FESA_INTEGRATION_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp") + +foreach(test_source IN LISTS FESA_INTEGRATION_TEST_SOURCES) + get_filename_component(test_name "${test_source}" NAME_WE) + add_executable("${test_name}" "${test_source}") + target_link_libraries("${test_name}" PRIVATE fesa_core) + add_test(NAME "${test_name}" COMMAND "${test_name}") +endforeach() diff --git a/tests/integration/solver_core_skeleton_integration_test.cpp b/tests/integration/solver_core_skeleton_integration_test.cpp new file mode 100644 index 0000000..ba47faf --- /dev/null +++ b/tests/integration/solver_core_skeleton_integration_test.cpp @@ -0,0 +1,64 @@ +#include +#include + +namespace { + +int fail() +{ + return 1; +} + +fesa::model::Domain make_domain() +{ + fesa::model::Domain domain; + domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}}); + domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}}); + domain.add_material({fesa::core::MaterialId{3}, "steel"}); + domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}}); + domain.add_element({ + fesa::core::ElementId{5}, + fesa::model::ElementTopology::truss2, + {fesa::core::NodeId{1}, fesa::core::NodeId{2}}, + fesa::core::PropertyId{4} + }); + fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static-step"}; + step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0}); + step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0}); + domain.add_step(step); + return domain; +} + +} // namespace + +int main() +{ + const auto domain = make_domain(); + fesa::analysis::LinearStaticAnalysis analysis{domain, fesa::core::StepId{6}}; + analysis.run(); + + if (analysis.analysis_model() == nullptr || analysis.state() == nullptr) { + return fail(); + } + if (analysis.analysis_model()->active_elements().size() != 1 || + analysis.analysis_model()->active_boundary_conditions().size() != 1 || + analysis.analysis_model()->active_loads().size() != 1) { + return fail(); + } + + fesa::results::ResultStep result_step{"static-step"}; + auto& frame = result_step.add_frame(0, 0.0); + frame.add_field_output({ + "U", + fesa::results::FieldLocation::nodal, + {"ux", "uy", "uz"}, + {1, 2}, + {0.0, 0.0, 0.0, 0.0, 0.0, 0.0} + }); + + if (result_step.frames().size() != 1 || + result_step.frames()[0].field_outputs().size() != 1) { + return fail(); + } + + return 0; +} diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 0000000..23d4cac --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,8 @@ +file(GLOB FESA_UNIT_TEST_SOURCES CONFIGURE_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/*_test.cpp") + +foreach(test_source IN LISTS FESA_UNIT_TEST_SOURCES) + get_filename_component(test_name "${test_source}" NAME_WE) + add_executable("${test_name}" "${test_source}") + target_link_libraries("${test_name}" PRIVATE fesa_core) + add_test(NAME "${test_name}" COMMAND "${test_name}") +endforeach() diff --git a/tests/unit/analysis_flow_linear_static_analysis_test.cpp b/tests/unit/analysis_flow_linear_static_analysis_test.cpp new file mode 100644 index 0000000..87e3325 --- /dev/null +++ b/tests/unit/analysis_flow_linear_static_analysis_test.cpp @@ -0,0 +1,37 @@ +#include + +namespace { + +fesa::model::Domain make_domain() +{ + fesa::model::Domain domain; + domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}}); + domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}}); + domain.add_material({fesa::core::MaterialId{3}, "steel"}); + domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}}); + domain.add_element({ + fesa::core::ElementId{5}, + fesa::model::ElementTopology::truss2, + {fesa::core::NodeId{1}, fesa::core::NodeId{2}}, + fesa::core::PropertyId{4} + }); + fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static"}; + step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0}); + step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0}); + domain.add_step(step); + return domain; +} + +} // namespace + +int main() +{ + const auto domain = make_domain(); + fesa::analysis::LinearStaticAnalysis analysis{domain, fesa::core::StepId{6}}; + analysis.run(); + + if (analysis.analysis_model() == nullptr || analysis.state() == nullptr) { + return 1; + } + return analysis.state()->displacement().size() == 6 ? 0 : 1; +} diff --git a/tests/unit/analysis_flow_template_test.cpp b/tests/unit/analysis_flow_template_test.cpp new file mode 100644 index 0000000..f944111 --- /dev/null +++ b/tests/unit/analysis_flow_template_test.cpp @@ -0,0 +1,44 @@ +#include + +#include +#include + +class RecordingAnalysis : public fesa::analysis::Analysis { +public: + const std::vector& calls() const + { + return calls_; + } + +protected: + void initialize() override { calls_.push_back("initialize"); } + void build_analysis_model() override { calls_.push_back("build_analysis_model"); } + void build_dof_map() override { calls_.push_back("build_dof_map"); } + void build_sparse_pattern() override { calls_.push_back("build_sparse_pattern"); } + void assemble() override { calls_.push_back("assemble"); } + void apply_boundary_conditions() override { calls_.push_back("apply_boundary_conditions"); } + void solve() override { calls_.push_back("solve"); } + void update_state() override { calls_.push_back("update_state"); } + void write_results() override { calls_.push_back("write_results"); } + +private: + std::vector calls_; +}; + +int main() +{ + RecordingAnalysis analysis; + analysis.run(); + const std::vector expected{ + "initialize", + "build_analysis_model", + "build_dof_map", + "build_sparse_pattern", + "assemble", + "apply_boundary_conditions", + "solve", + "update_state", + "write_results" + }; + return analysis.calls() == expected ? 0 : 1; +} diff --git a/tests/unit/analysis_model_view_test.cpp b/tests/unit/analysis_model_view_test.cpp new file mode 100644 index 0000000..1af1817 --- /dev/null +++ b/tests/unit/analysis_model_view_test.cpp @@ -0,0 +1,73 @@ +#include + +#include + +namespace { + +fesa::model::Domain make_domain() +{ + fesa::model::Domain domain; + domain.add_node({fesa::core::NodeId{1}, {0.0, 0.0, 0.0}}); + domain.add_node({fesa::core::NodeId{2}, {1.0, 0.0, 0.0}}); + domain.add_material({fesa::core::MaterialId{3}, "steel"}); + domain.add_property({fesa::core::PropertyId{4}, "bar", fesa::core::MaterialId{3}}); + domain.add_element({ + fesa::core::ElementId{5}, + fesa::model::ElementTopology::truss2, + {fesa::core::NodeId{1}, fesa::core::NodeId{2}}, + fesa::core::PropertyId{4} + }); + + fesa::model::AnalysisStep step{fesa::core::StepId{6}, "static"}; + step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0}); + step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0}); + domain.add_step(step); + return domain; +} + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + const auto domain = make_domain(); + const fesa::analysis::AnalysisModel model{domain, fesa::core::StepId{6}}; + + if (&model.domain() != &domain) { + return fail(); + } + if (&model.step() != domain.find_step(fesa::core::StepId{6})) { + return fail(); + } + if (model.active_elements().size() != 1 || + model.active_elements()[0] != domain.find_element(fesa::core::ElementId{5})) { + return fail(); + } + if (model.active_boundary_conditions().size() != 1 || + model.active_loads().size() != 1) { + return fail(); + } + + const auto* property = model.property_for(*model.active_elements()[0]); + if (property == nullptr || property != domain.find_property(fesa::core::PropertyId{4})) { + return fail(); + } + + const auto* material = model.material_for(*property); + if (material == nullptr || material != domain.find_material(fesa::core::MaterialId{3})) { + return fail(); + } + + try { + const fesa::analysis::AnalysisModel missing{domain, fesa::core::StepId{99}}; + (void)missing; + return fail(); + } catch (const std::invalid_argument&) { + } + + return 0; +} diff --git a/tests/unit/analysis_state_vectors_test.cpp b/tests/unit/analysis_state_vectors_test.cpp new file mode 100644 index 0000000..61f2fa3 --- /dev/null +++ b/tests/unit/analysis_state_vectors_test.cpp @@ -0,0 +1,80 @@ +#include + +#include +#include + +namespace { + +int fail() +{ + return 1; +} + +bool all_zero(const std::vector& values) +{ + for (const double value : values) { + if (value != 0.0) { + return false; + } + } + return true; +} + +} // namespace + +int main() +{ + fesa::analysis::AnalysisState state{3}; + if (state.displacement().size() != 3 || + state.velocity().size() != 3 || + state.acceleration().size() != 3 || + state.temperature().size() != 3 || + state.external_force().size() != 3 || + state.internal_force().size() != 3 || + state.residual().size() != 3) { + return fail(); + } + if (!all_zero(state.displacement()) || !all_zero(state.residual())) { + return fail(); + } + + state.set_displacement({1.0, 2.0, 3.0}); + if (state.displacement()[2] != 3.0) { + return fail(); + } + + state.set_external_force({10.0, 20.0, 30.0}); + state.set_internal_force({1.0, 2.0, 3.0}); + state.update_residual(); + if (state.residual()[0] != 9.0 || + state.residual()[1] != 18.0 || + state.residual()[2] != 27.0) { + return fail(); + } + + try { + state.set_displacement({1.0}); + return fail(); + } catch (const std::invalid_argument&) { + } + + state.iteration_state().time = 1.25; + state.iteration_state().increment = 2; + state.iteration_state().iteration = 3; + if (state.iteration_state().time != 1.25 || + state.iteration_state().increment != 2 || + state.iteration_state().iteration != 3) { + return fail(); + } + + state.set_element_state(fesa::core::ElementId{7}, {4.0, 5.0}); + const auto* element_state = state.element_state(fesa::core::ElementId{7}); + if (element_state == nullptr || element_state->size() != 2 || (*element_state)[1] != 5.0) { + return fail(); + } + if (state.element_state(fesa::core::ElementId{8}) != nullptr) { + return fail(); + } + + return 0; +} diff --git a/tests/unit/core_diagnostic_test.cpp b/tests/unit/core_diagnostic_test.cpp new file mode 100644 index 0000000..b33f6ca --- /dev/null +++ b/tests/unit/core_diagnostic_test.cpp @@ -0,0 +1,11 @@ +#include + +int main() +{ + const fesa::core::Diagnostic diagnostic{ + fesa::core::Severity::info, + "core.info", + "diagnostic" + }; + return diagnostic.code == "core.info" ? 0 : 1; +} diff --git a/tests/unit/core_ids_test.cpp b/tests/unit/core_ids_test.cpp new file mode 100644 index 0000000..541ff04 --- /dev/null +++ b/tests/unit/core_ids_test.cpp @@ -0,0 +1,9 @@ +#include + +#include + +int main() +{ + static_assert(!std::is_same_v); + return fesa::core::NodeId{7}.value == 7 ? 0 : 1; +} diff --git a/tests/unit/core_primitives_test.cpp b/tests/unit/core_primitives_test.cpp new file mode 100644 index 0000000..32a3ead --- /dev/null +++ b/tests/unit/core_primitives_test.cpp @@ -0,0 +1,50 @@ +#include +#include +#include + +#include +#include + +namespace { + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + static_assert(!std::is_same_v); + + const auto ok = fesa::core::Status::ok(); + if (!ok.is_ok() || !ok.diagnostics().empty()) { + return fail(); + } + + fesa::core::Diagnostic error{ + fesa::core::Severity::error, + "core.error", + "core failure" + }; + auto failed = fesa::core::Status::failure(error); + if (failed.is_ok() || failed.diagnostics().size() != 1) { + return fail(); + } + if (failed.diagnostics()[0].code != "core.error" || + failed.diagnostics()[0].message != "core failure") { + return fail(); + } + + failed.add({fesa::core::Severity::warning, "core.warning", "check warning"}); + if (failed.diagnostics().size() != 2) { + return fail(); + } + if (failed.diagnostics()[0].code != "core.error" || + failed.diagnostics()[1].code != "core.warning") { + return fail(); + } + + return 0; +} diff --git a/tests/unit/core_status_test.cpp b/tests/unit/core_status_test.cpp new file mode 100644 index 0000000..31153fb --- /dev/null +++ b/tests/unit/core_status_test.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + const auto status = fesa::core::Status::ok(); + return status.is_ok() ? 0 : 1; +} diff --git a/tests/unit/dof_manager_dof_key_test.cpp b/tests/unit/dof_manager_dof_key_test.cpp new file mode 100644 index 0000000..adca3cc --- /dev/null +++ b/tests/unit/dof_manager_dof_key_test.cpp @@ -0,0 +1,8 @@ +#include + +int main() +{ + const fesa::fem::DofKey lhs{fesa::core::NodeId{1}, fesa::model::DofComponent::ux}; + const fesa::fem::DofKey rhs{fesa::core::NodeId{1}, fesa::model::DofComponent::ux}; + return lhs == rhs ? 0 : 1; +} diff --git a/tests/unit/dof_manager_numbering_test.cpp b/tests/unit/dof_manager_numbering_test.cpp new file mode 100644 index 0000000..73b3b53 --- /dev/null +++ b/tests/unit/dof_manager_numbering_test.cpp @@ -0,0 +1,83 @@ +#include + +#include +#include + +namespace { + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + fesa::fem::DofManager dofs; + dofs.define_node_dofs(fesa::core::NodeId{2}, { + fesa::model::DofComponent::ux, + fesa::model::DofComponent::uy + }); + dofs.define_node_dofs(fesa::core::NodeId{1}, { + fesa::model::DofComponent::uy, + fesa::model::DofComponent::ux + }); + dofs.apply_boundary_condition({ + fesa::core::NodeId{1}, + fesa::model::DofComponent::ux, + 0.0 + }); + dofs.number_equations(); + + const fesa::fem::DofKey n1ux{fesa::core::NodeId{1}, fesa::model::DofComponent::ux}; + const fesa::fem::DofKey n1uy{fesa::core::NodeId{1}, fesa::model::DofComponent::uy}; + const fesa::fem::DofKey n2ux{fesa::core::NodeId{2}, fesa::model::DofComponent::ux}; + const fesa::fem::DofKey n2uy{fesa::core::NodeId{2}, fesa::model::DofComponent::uy}; + + if (dofs.total_dof_count() != 4 || + dofs.constrained_dof_count() != 1 || + dofs.free_dof_count() != 3) { + return fail(); + } + if (dofs.equation_id(n1ux) != 0 || + dofs.equation_id(n1uy) != 1 || + dofs.equation_id(n2ux) != 2 || + dofs.equation_id(n2uy) != 3) { + return fail(); + } + if (!dofs.is_constrained(n1ux) || + dofs.free_equation_id(n1ux).has_value()) { + return fail(); + } + if (!dofs.free_equation_id(n1uy).has_value() || + *dofs.free_equation_id(n1uy) != 0 || + *dofs.free_equation_id(n2ux) != 1 || + *dofs.free_equation_id(n2uy) != 2) { + return fail(); + } + + const auto full = dofs.expand_free_vector({11.0, 22.0, 33.0}); + if (full.size() != 4 || + full[0] != 0.0 || + full[1] != 11.0 || + full[2] != 22.0 || + full[3] != 33.0) { + return fail(); + } + + const auto& pattern = dofs.sparse_pattern(); + if (pattern.size() != 9 || + pattern.front() != std::pair{0, 0} || + pattern.back() != std::pair{2, 2}) { + return fail(); + } + + try { + (void)dofs.equation_id({fesa::core::NodeId{99}, fesa::model::DofComponent::ux}); + return fail(); + } catch (const std::invalid_argument&) { + } + + return 0; +} diff --git a/tests/unit/harness_smoke_test.cpp b/tests/unit/harness_smoke_test.cpp new file mode 100644 index 0000000..e54d8f6 --- /dev/null +++ b/tests/unit/harness_smoke_test.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + const std::string project = "fesa"; + return project.size() == 4 ? 0 : 1; +} diff --git a/tests/unit/model_analysis_step_test.cpp b/tests/unit/model_analysis_step_test.cpp new file mode 100644 index 0000000..0f363c3 --- /dev/null +++ b/tests/unit/model_analysis_step_test.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + const fesa::model::AnalysisStep step{fesa::core::StepId{1}, "static"}; + return step.name() == "static" ? 0 : 1; +} diff --git a/tests/unit/model_boundary_condition_test.cpp b/tests/unit/model_boundary_condition_test.cpp new file mode 100644 index 0000000..913b817 --- /dev/null +++ b/tests/unit/model_boundary_condition_test.cpp @@ -0,0 +1,11 @@ +#include + +int main() +{ + const fesa::model::BoundaryCondition bc{ + fesa::core::NodeId{1}, + fesa::model::DofComponent::ux, + 0.0 + }; + return bc.node_id().value == 1 ? 0 : 1; +} diff --git a/tests/unit/model_domain_test.cpp b/tests/unit/model_domain_test.cpp new file mode 100644 index 0000000..514f855 --- /dev/null +++ b/tests/unit/model_domain_test.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include + +namespace { + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + fesa::model::Domain domain; + + domain.add_node(fesa::model::Node{fesa::core::NodeId{1}, {1.0, 2.0, 3.0}}); + domain.add_element(fesa::model::Element{ + fesa::core::ElementId{10}, + fesa::model::ElementTopology::truss2, + {fesa::core::NodeId{1}, fesa::core::NodeId{2}}, + fesa::core::PropertyId{20} + }); + domain.add_material(fesa::model::Material{fesa::core::MaterialId{30}, "steel"}); + domain.add_property(fesa::model::Property{ + fesa::core::PropertyId{20}, + "bar", + fesa::core::MaterialId{30} + }); + + fesa::model::AnalysisStep step{fesa::core::StepId{40}, "load"}; + step.add_boundary_condition({fesa::core::NodeId{1}, fesa::model::DofComponent::ux, 0.0}); + step.add_load({fesa::core::NodeId{2}, fesa::model::DofComponent::ux, 10.0}); + domain.add_step(step); + + const auto* node = domain.find_node(fesa::core::NodeId{1}); + if (node == nullptr || node->coordinates()[2] != 3.0) { + return fail(); + } + + const auto* element = domain.find_element(fesa::core::ElementId{10}); + if (element == nullptr || + element->topology() != fesa::model::ElementTopology::truss2 || + element->node_ids().size() != 2 || + element->property_id().value != 20) { + return fail(); + } + + const auto* material = domain.find_material(fesa::core::MaterialId{30}); + if (material == nullptr || material->name() != "steel") { + return fail(); + } + + const auto* property = domain.find_property(fesa::core::PropertyId{20}); + if (property == nullptr || + property->name() != "bar" || + property->material_id().value != 30) { + return fail(); + } + + const auto* analysis_step = domain.find_step(fesa::core::StepId{40}); + if (analysis_step == nullptr || + analysis_step->boundary_conditions().size() != 1 || + analysis_step->loads().size() != 1) { + return fail(); + } + + if (domain.find_node(fesa::core::NodeId{999}) != nullptr || + domain.find_element(fesa::core::ElementId{999}) != nullptr || + domain.find_material(fesa::core::MaterialId{999}) != nullptr || + domain.find_property(fesa::core::PropertyId{999}) != nullptr || + domain.find_step(fesa::core::StepId{999}) != nullptr) { + return fail(); + } + + return 0; +} diff --git a/tests/unit/model_element_test.cpp b/tests/unit/model_element_test.cpp new file mode 100644 index 0000000..d66c7fc --- /dev/null +++ b/tests/unit/model_element_test.cpp @@ -0,0 +1,12 @@ +#include + +int main() +{ + const fesa::model::Element element{ + fesa::core::ElementId{1}, + fesa::model::ElementTopology::bar2, + {fesa::core::NodeId{1}, fesa::core::NodeId{2}}, + fesa::core::PropertyId{3} + }; + return element.node_ids().size() == 2 ? 0 : 1; +} diff --git a/tests/unit/model_load_test.cpp b/tests/unit/model_load_test.cpp new file mode 100644 index 0000000..ebc15d8 --- /dev/null +++ b/tests/unit/model_load_test.cpp @@ -0,0 +1,11 @@ +#include + +int main() +{ + const fesa::model::Load load{ + fesa::core::NodeId{2}, + fesa::model::DofComponent::ux, + 5.0 + }; + return load.value() == 5.0 ? 0 : 1; +} diff --git a/tests/unit/model_material_test.cpp b/tests/unit/model_material_test.cpp new file mode 100644 index 0000000..9a4625c --- /dev/null +++ b/tests/unit/model_material_test.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + const fesa::model::Material material{fesa::core::MaterialId{1}, "steel"}; + return material.name() == "steel" ? 0 : 1; +} diff --git a/tests/unit/model_node_test.cpp b/tests/unit/model_node_test.cpp new file mode 100644 index 0000000..dbdcfee --- /dev/null +++ b/tests/unit/model_node_test.cpp @@ -0,0 +1,7 @@ +#include + +int main() +{ + const fesa::model::Node node{fesa::core::NodeId{1}, {0.0, 1.0, 2.0}}; + return node.coordinates()[1] == 1.0 ? 0 : 1; +} diff --git a/tests/unit/model_property_test.cpp b/tests/unit/model_property_test.cpp new file mode 100644 index 0000000..6ab919c --- /dev/null +++ b/tests/unit/model_property_test.cpp @@ -0,0 +1,11 @@ +#include + +int main() +{ + const fesa::model::Property property{ + fesa::core::PropertyId{2}, + "section", + fesa::core::MaterialId{3} + }; + return property.material_id().value == 3 ? 0 : 1; +} diff --git a/tests/unit/results_containers_test.cpp b/tests/unit/results_containers_test.cpp new file mode 100644 index 0000000..8e7cc59 --- /dev/null +++ b/tests/unit/results_containers_test.cpp @@ -0,0 +1,69 @@ +#include + +#include + +namespace { + +int fail() +{ + return 1; +} + +} // namespace + +int main() +{ + fesa::results::ResultStep step{"static"}; + auto& frame = step.add_frame(1, 0.0); + if (step.name() != "static" || + step.frames().size() != 1 || + frame.frame_id() != 1 || + frame.time() != 0.0) { + return fail(); + } + + frame.add_field_output({ + "U", + fesa::results::FieldLocation::nodal, + {"ux", "uy"}, + {1, 2}, + {0.0, 0.1, 1.0, 1.1} + }); + if (frame.field_outputs().size() != 1 || + frame.field_outputs()[0].entity_ids[1] != 2 || + frame.field_outputs()[0].values[3] != 1.1) { + return fail(); + } + + frame.add_history_output({"load-factor", {0.0, 1.0}, {0.0, 10.0}}); + if (frame.history_outputs().size() != 1 || + frame.history_outputs()[0].values[1] != 10.0) { + return fail(); + } + + try { + frame.add_field_output({ + "bad", + fesa::results::FieldLocation::nodal, + {}, + {1}, + {0.0} + }); + return fail(); + } catch (const std::invalid_argument&) { + } + + try { + frame.add_field_output({ + "bad-shape", + fesa::results::FieldLocation::nodal, + {"ux", "uy"}, + {1}, + {0.0} + }); + return fail(); + } catch (const std::invalid_argument&) { + } + + return 0; +}