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
+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;
}