feat: add solver core skeleton

This commit is contained in:
NINI
2026-06-12 02:25:07 +09:00
parent 4e7fd1087d
commit cbd1a6c5d7
46 changed files with 1911 additions and 19 deletions
+10
View File
@@ -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)
@@ -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.
+1 -1
View File
@@ -2,7 +2,7 @@
"phases": [ "phases": [
{ {
"dir": "solver-core-skeleton", "dir": "solver-core-skeleton",
"status": "pending" "status": "completed"
} }
] ]
} }
+31 -18
View File
@@ -9,79 +9,92 @@
"allowed_paths": [ "allowed_paths": [
"CMakeLists.txt", "CMakeLists.txt",
"tests/" "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, "step": 1,
"name": "core-primitives", "name": "core-primitives",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/core/", "src/fesa/core/",
"tests/unit/core_*_test.cpp" "tests/unit/core_*_test.cpp"
] ],
"summary": "Core ID, diagnostic, and status primitives added with tests"
}, },
{ {
"step": 2, "step": 2,
"name": "domain-model-entities", "name": "domain-model-entities",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/model/", "src/fesa/model/",
"tests/unit/model_*_test.cpp" "tests/unit/model_*_test.cpp"
] ],
"summary": "Model entities and Domain ownership API added with tests"
}, },
{ {
"step": 3, "step": 3,
"name": "analysis-model-view", "name": "analysis-model-view",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/analysis/", "src/fesa/analysis/",
"tests/unit/analysis_model_*_test.cpp" "tests/unit/analysis_model_*_test.cpp"
] ],
"summary": "AnalysisModel step view added without Domain copies"
}, },
{ {
"step": 4, "step": 4,
"name": "dof-manager", "name": "dof-manager",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/fem/", "src/fesa/fem/",
"tests/unit/dof_manager_*_test.cpp" "tests/unit/dof_manager_*_test.cpp"
] ],
"summary": "DofManager deterministic numbering and constrained/free mapping added"
}, },
{ {
"step": 5, "step": 5,
"name": "analysis-state", "name": "analysis-state",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/analysis/", "src/fesa/analysis/",
"tests/unit/analysis_state_*_test.cpp" "tests/unit/analysis_state_*_test.cpp"
] ],
"summary": "AnalysisState vector ownership and residual update added"
}, },
{ {
"step": 6, "step": 6,
"name": "analysis-template-flow", "name": "analysis-template-flow",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/analysis/", "src/fesa/analysis/",
"tests/unit/analysis_flow_*_test.cpp" "tests/unit/analysis_flow_*_test.cpp"
] ],
"summary": "Analysis template method and LinearStaticAnalysis skeleton added"
}, },
{ {
"step": 7, "step": 7,
"name": "results-containers", "name": "results-containers",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"src/fesa/results/", "src/fesa/results/",
"tests/unit/results_*_test.cpp" "tests/unit/results_*_test.cpp"
] ],
"summary": "ResultStep, ResultFrame, FieldOutput, and HistoryOutput containers added"
}, },
{ {
"step": 8, "step": 8,
"name": "solver-skeleton-integration-report", "name": "solver-skeleton-integration-report",
"status": "pending", "status": "completed",
"allowed_paths": [ "allowed_paths": [
"tests/integration/", "tests/integration/",
"docs/build-test-reports/" "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"
} }
+34
View File
@@ -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
+73
View File
@@ -0,0 +1,73 @@
#pragma once
#include <fesa/model/domain.hpp>
#include <stdexcept>
#include <vector>
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<const model::Element*>& active_elements() const
{
return active_elements_;
}
const std::vector<const model::BoundaryCondition*>& active_boundary_conditions() const
{
return active_boundary_conditions_;
}
const std::vector<const model::Load*>& 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<const model::Element*> active_elements_;
std::vector<const model::BoundaryCondition*> active_boundary_conditions_;
std::vector<const model::Load*> active_loads_;
};
} // namespace fesa::analysis
+146
View File
@@ -0,0 +1,146 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <stdexcept>
#include <utility>
#include <vector>
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<double>& displacement() const
{
return displacement_;
}
const std::vector<double>& velocity() const
{
return velocity_;
}
const std::vector<double>& acceleration() const
{
return acceleration_;
}
const std::vector<double>& temperature() const
{
return temperature_;
}
const std::vector<double>& external_force() const
{
return external_force_;
}
const std::vector<double>& internal_force() const
{
return internal_force_;
}
const std::vector<double>& residual() const
{
return residual_;
}
void set_displacement(std::vector<double> values)
{
assign_same_size(displacement_, std::move(values));
}
void set_external_force(std::vector<double> values)
{
assign_same_size(external_force_, std::move(values));
}
void set_internal_force(std::vector<double> 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<double> 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<double>* 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<double> vector_of(int size)
{
if (size < 0) {
throw std::invalid_argument("negative dof count");
}
return std::vector<double>(static_cast<std::size_t>(size), 0.0);
}
static void assign_same_size(std::vector<double>& target, std::vector<double> values)
{
if (target.size() != values.size()) {
throw std::invalid_argument("vector size mismatch");
}
target = std::move(values);
}
std::vector<double> displacement_;
std::vector<double> velocity_;
std::vector<double> acceleration_;
std::vector<double> temperature_;
std::vector<double> external_force_;
std::vector<double> internal_force_;
std::vector<double> residual_;
IterationState iteration_state_;
std::vector<std::pair<core::ElementId, std::vector<double>>> element_states_;
};
} // namespace fesa::analysis
@@ -0,0 +1,66 @@
#pragma once
#include <fesa/analysis/analysis.hpp>
#include <fesa/analysis/analysis_model.hpp>
#include <fesa/analysis/analysis_state.hpp>
#include <fesa/fem/dof_manager.hpp>
#include <memory>
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<AnalysisModel>(domain_, step_id_);
}
void build_dof_map() override
{
dof_manager_ = std::make_unique<fem::DofManager>();
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<AnalysisState>(dof_manager_->total_dof_count());
}
private:
const model::Domain& domain_;
core::StepId step_id_;
std::unique_ptr<AnalysisModel> analysis_model_;
std::unique_ptr<fem::DofManager> dof_manager_;
std::unique_ptr<AnalysisState> state_;
};
} // namespace fesa::analysis
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <string>
namespace fesa::core {
enum class Severity {
info,
warning,
error
};
struct Diagnostic {
Severity severity;
std::string code;
std::string message;
};
} // namespace fesa::core
+25
View File
@@ -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
+43
View File
@@ -0,0 +1,43 @@
#pragma once
#include <fesa/core/diagnostic.hpp>
#include <utility>
#include <vector>
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<Diagnostic>& diagnostics() const
{
return diagnostics_;
}
void add(Diagnostic diagnostic)
{
diagnostics_.push_back(std::move(diagnostic));
}
private:
std::vector<Diagnostic> diagnostics_;
};
} // namespace fesa::core
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <fesa/model/boundary_condition.hpp>
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
+151
View File
@@ -0,0 +1,151 @@
#pragma once
#include <fesa/fem/dof_key.hpp>
#include <algorithm>
#include <optional>
#include <stdexcept>
#include <utility>
#include <vector>
namespace fesa::fem {
class DofManager {
public:
void define_node_dofs(core::NodeId node_id, std::vector<model::DofComponent> 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<int>(lhs.key.component) < static_cast<int>(rhs.key.component);
});
int free_id = 0;
for (int equation_id = 0; equation_id < static_cast<int>(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<int>(records_.size());
}
int free_dof_count() const
{
return static_cast<int>(
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<int> free_equation_id(DofKey key) const
{
return require_record(key).free_equation_id;
}
std::vector<double> expand_free_vector(const std::vector<double>& free_values) const
{
if (free_values.size() != static_cast<std::size_t>(free_dof_count())) {
throw std::invalid_argument("free vector size does not match dof manager");
}
std::vector<double> full(records_.size(), 0.0);
for (const auto& record : records_) {
if (record.free_equation_id.has_value()) {
full[static_cast<std::size_t>(record.equation_id)] =
free_values[static_cast<std::size_t>(*record.free_equation_id)];
}
}
return full;
}
const std::vector<std::pair<int, int>>& sparse_pattern() const
{
return sparse_pattern_;
}
private:
struct Record {
DofKey key;
bool constrained = false;
int equation_id = -1;
std::optional<int> free_equation_id;
};
std::vector<Record>::iterator find_record(DofKey key)
{
return std::find_if(records_.begin(), records_.end(), [key](const Record& record) {
return record.key == key;
});
}
std::vector<Record>::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<Record> records_;
std::vector<std::pair<int, int>> sparse_pattern_;
};
} // namespace fesa::fem
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <fesa/model/boundary_condition.hpp>
#include <fesa/model/load.hpp>
#include <string>
#include <utility>
#include <vector>
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<BoundaryCondition>& boundary_conditions() const
{
return boundary_conditions_;
}
const std::vector<Load>& loads() const
{
return loads_;
}
private:
core::StepId id_;
std::string name_;
std::vector<BoundaryCondition> boundary_conditions_;
std::vector<Load> loads_;
};
} // namespace fesa::model
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <fesa/core/ids.hpp>
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
+124
View File
@@ -0,0 +1,124 @@
#pragma once
#include <fesa/model/analysis_step.hpp>
#include <fesa/model/element.hpp>
#include <fesa/model/material.hpp>
#include <fesa/model/node.hpp>
#include <fesa/model/property.hpp>
#include <utility>
#include <vector>
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<Node>& nodes() const
{
return nodes_;
}
const std::vector<Element>& elements() const
{
return elements_;
}
const std::vector<Material>& materials() const
{
return materials_;
}
const std::vector<Property>& properties() const
{
return properties_;
}
const std::vector<AnalysisStep>& 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<Node> nodes_;
std::vector<Element> elements_;
std::vector<Material> materials_;
std::vector<Property> properties_;
std::vector<AnalysisStep> steps_;
};
} // namespace fesa::model
+56
View File
@@ -0,0 +1,56 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <utility>
#include <vector>
namespace fesa::model {
enum class ElementTopology {
truss2,
bar2,
unknown
};
class Element {
public:
Element(core::ElementId id,
ElementTopology topology,
std::vector<core::NodeId> 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<core::NodeId>& node_ids() const
{
return node_ids_;
}
core::PropertyId property_id() const
{
return property_id_;
}
private:
core::ElementId id_;
ElementTopology topology_;
std::vector<core::NodeId> node_ids_;
core::PropertyId property_id_;
};
} // namespace fesa::model
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <fesa/model/boundary_condition.hpp>
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
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <string>
#include <utility>
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
+31
View File
@@ -0,0 +1,31 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <array>
namespace fesa::model {
class Node {
public:
Node(core::NodeId id, std::array<double, 3> coordinates)
: id_(id), coordinates_(coordinates)
{
}
core::NodeId id() const
{
return id_;
}
const std::array<double, 3>& coordinates() const
{
return coordinates_;
}
private:
core::NodeId id_;
std::array<double, 3> coordinates_;
};
} // namespace fesa::model
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include <fesa/core/ids.hpp>
#include <string>
#include <utility>
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
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
namespace fesa::results {
enum class FieldLocation {
nodal,
element,
integration_point
};
struct FieldOutput {
std::string name;
FieldLocation location;
std::vector<std::string> components;
std::vector<int> entity_ids;
std::vector<double> values;
};
struct HistoryOutput {
std::string name;
std::vector<double> time;
std::vector<double> 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<FieldOutput>& field_outputs() const
{
return field_outputs_;
}
const std::vector<HistoryOutput>& history_outputs() const
{
return history_outputs_;
}
private:
int frame_id_;
double time_;
std::vector<FieldOutput> field_outputs_;
std::vector<HistoryOutput> 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<ResultFrame>& frames() const
{
return frames_;
}
private:
std::string name_;
std::vector<ResultFrame> frames_;
};
} // namespace fesa::results
+2
View File
@@ -0,0 +1,2 @@
add_subdirectory(unit)
add_subdirectory(integration)
+8
View File
@@ -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()
@@ -0,0 +1,64 @@
#include <fesa/analysis/linear_static_analysis.hpp>
#include <fesa/results/results.hpp>
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;
}
+8
View File
@@ -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()
@@ -0,0 +1,37 @@
#include <fesa/analysis/linear_static_analysis.hpp>
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;
}
@@ -0,0 +1,44 @@
#include <fesa/analysis/analysis.hpp>
#include <string>
#include <vector>
class RecordingAnalysis : public fesa::analysis::Analysis {
public:
const std::vector<std::string>& 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<std::string> calls_;
};
int main()
{
RecordingAnalysis analysis;
analysis.run();
const std::vector<std::string> 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;
}
+73
View File
@@ -0,0 +1,73 @@
#include <fesa/analysis/analysis_model.hpp>
#include <stdexcept>
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;
}
@@ -0,0 +1,80 @@
#include <fesa/analysis/analysis_state.hpp>
#include <stdexcept>
#include <vector>
namespace {
int fail()
{
return 1;
}
bool all_zero(const std::vector<double>& 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;
}
+11
View File
@@ -0,0 +1,11 @@
#include <fesa/core/diagnostic.hpp>
int main()
{
const fesa::core::Diagnostic diagnostic{
fesa::core::Severity::info,
"core.info",
"diagnostic"
};
return diagnostic.code == "core.info" ? 0 : 1;
}
+9
View File
@@ -0,0 +1,9 @@
#include <fesa/core/ids.hpp>
#include <type_traits>
int main()
{
static_assert(!std::is_same_v<fesa::core::NodeId, fesa::core::ElementId>);
return fesa::core::NodeId{7}.value == 7 ? 0 : 1;
}
+50
View File
@@ -0,0 +1,50 @@
#include <fesa/core/diagnostic.hpp>
#include <fesa/core/ids.hpp>
#include <fesa/core/status.hpp>
#include <string>
#include <type_traits>
namespace {
int fail()
{
return 1;
}
} // namespace
int main()
{
static_assert(!std::is_same_v<fesa::core::NodeId, fesa::core::ElementId>);
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;
}
+7
View File
@@ -0,0 +1,7 @@
#include <fesa/core/status.hpp>
int main()
{
const auto status = fesa::core::Status::ok();
return status.is_ok() ? 0 : 1;
}
+8
View File
@@ -0,0 +1,8 @@
#include <fesa/fem/dof_key.hpp>
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;
}
+83
View File
@@ -0,0 +1,83 @@
#include <fesa/fem/dof_manager.hpp>
#include <stdexcept>
#include <vector>
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<int, int>{0, 0} ||
pattern.back() != std::pair<int, int>{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;
}
+7
View File
@@ -0,0 +1,7 @@
#include <string>
int main()
{
const std::string project = "fesa";
return project.size() == 4 ? 0 : 1;
}
+7
View File
@@ -0,0 +1,7 @@
#include <fesa/model/analysis_step.hpp>
int main()
{
const fesa::model::AnalysisStep step{fesa::core::StepId{1}, "static"};
return step.name() == "static" ? 0 : 1;
}
@@ -0,0 +1,11 @@
#include <fesa/model/boundary_condition.hpp>
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;
}
+79
View File
@@ -0,0 +1,79 @@
#include <fesa/model/domain.hpp>
#include <array>
#include <vector>
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;
}
+12
View File
@@ -0,0 +1,12 @@
#include <fesa/model/element.hpp>
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;
}
+11
View File
@@ -0,0 +1,11 @@
#include <fesa/model/load.hpp>
int main()
{
const fesa::model::Load load{
fesa::core::NodeId{2},
fesa::model::DofComponent::ux,
5.0
};
return load.value() == 5.0 ? 0 : 1;
}
+7
View File
@@ -0,0 +1,7 @@
#include <fesa/model/material.hpp>
int main()
{
const fesa::model::Material material{fesa::core::MaterialId{1}, "steel"};
return material.name() == "steel" ? 0 : 1;
}
+7
View File
@@ -0,0 +1,7 @@
#include <fesa/model/node.hpp>
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;
}
+11
View File
@@ -0,0 +1,11 @@
#include <fesa/model/property.hpp>
int main()
{
const fesa::model::Property property{
fesa::core::PropertyId{2},
"section",
fesa::core::MaterialId{3}
};
return property.material_id().value == 3 ? 0 : 1;
}
+69
View File
@@ -0,0 +1,69 @@
#include <fesa/results/results.hpp>
#include <stdexcept>
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;
}