Files
FESADev/tests/test_main.cpp
T
2026-05-04 22:02:25 +09:00

1137 lines
46 KiB
C++

#include "fesa/fesa.hpp"
#include <array>
#include <cmath>
#include <functional>
#include <iostream>
#include <type_traits>
#include <stdexcept>
#include <string>
#include <vector>
static_assert(std::is_same_v<fesa::Real, double>, "Real must remain double");
static_assert(std::is_same_v<fesa::GlobalId, std::int64_t>, "GlobalId must remain int64");
static_assert(std::is_same_v<fesa::LocalIndex, std::int64_t>, "LocalIndex must remain int64");
static_assert(std::is_same_v<fesa::EquationId, std::int64_t>, "EquationId must remain int64");
static_assert(std::is_same_v<fesa::SparseIndex, std::int64_t>, "SparseIndex must remain int64");
namespace {
using TestFn = std::function<void()>;
struct TestCase {
std::string name;
TestFn fn;
};
std::vector<TestCase>& registry() {
static std::vector<TestCase> tests;
return tests;
}
struct RegisterTest {
RegisterTest(std::string name, TestFn fn) {
registry().push_back({std::move(name), std::move(fn)});
}
};
#define FESA_TEST(name) \
void name(); \
RegisterTest register_##name(#name, name); \
void name()
#define FESA_CHECK(expr) \
do { \
if (!(expr)) { \
throw std::runtime_error(std::string("check failed: ") + #expr); \
} \
} while (false)
#define FESA_CHECK_NEAR(actual, expected, tol) \
do { \
const auto actual_value = (actual); \
const auto expected_value = (expected); \
if (std::fabs(actual_value - expected_value) > (tol)) { \
throw std::runtime_error(std::string("near check failed: ") + #actual); \
} \
} while (false)
std::string sourceRoot() {
#ifdef FESA_SOURCE_DIR
return FESA_SOURCE_DIR;
#else
return ".";
#endif
}
std::string phase1Input() {
return R"inp(
*Node
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element, type=S4, elset=EALL
1, 1, 2, 3, 4
*Nset, nset=LEFT
1, 4
*Nset, nset=RIGHT
2, 3
*Elset, elset=EALL
1
*Material, name=STEEL
*Elastic
1000.0, 0.3
*Shell Section, elset=EALL, material=STEEL
0.1
*Boundary
LEFT, 1, 6, 0
RIGHT, 1, 2, 0
RIGHT, 4, 6, 0
*Cload
2, 3, -1
3, 3, -1
*Step, name=Step-1
*Static
*End Step
)inp";
}
fesa::Domain parsedPhase1Domain() {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(phase1Input());
FESA_CHECK(parsed.ok());
auto diagnostics = fesa::validateDomain(parsed.domain);
FESA_CHECK(!fesa::hasError(diagnostics));
return parsed.domain;
}
const fesa::Diagnostic* findDiagnostic(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
for (const auto& diagnostic : diagnostics) {
if (diagnostic.code == code) {
return &diagnostic;
}
}
return nullptr;
}
std::size_t diagnosticCount(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
std::size_t count = 0;
for (const auto& diagnostic : diagnostics) {
if (diagnostic.code == code) {
++count;
}
}
return count;
}
void checkVecNear(const fesa::Vec3& actual, const fesa::Vec3& expected, fesa::Real tolerance) {
FESA_CHECK_NEAR(actual.x, expected.x, tolerance);
FESA_CHECK_NEAR(actual.y, expected.y, tolerance);
FESA_CHECK_NEAR(actual.z, expected.z, tolerance);
}
void checkRightHandedOrthonormal(const fesa::Vec3& e1, const fesa::Vec3& e2, const fesa::Vec3& e3) {
FESA_CHECK_NEAR(fesa::norm(e1), 1.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::norm(e2), 1.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::norm(e3), 1.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::dot(e1, e2), 0.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::dot(e1, e3), 0.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::dot(e2, e3), 0.0, 1.0e-14);
FESA_CHECK_NEAR(fesa::dot(fesa::cross(e1, e2), e3), 1.0, 1.0e-14);
}
std::array<fesa::Real, 24> zeroElementDofs() {
std::array<fesa::Real, 24> values{};
values.fill(0.0);
return values;
}
fesa::Domain singleElementValidationDomain() {
fesa::Domain domain;
domain.nodes[1] = {1, {0, 0, 0}};
domain.nodes[2] = {2, {1, 0, 0}};
domain.nodes[3] = {3, {1, 1, 0}};
domain.nodes[4] = {4, {0, 1, 0}};
domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 4}, "EALL"};
domain.element_sets["eall"] = {"EALL", {1}};
domain.node_sets["all_nodes"] = {"ALL_NODES", {1, 2, 3, 4}};
domain.materials["mat"] = {"MAT", 1000.0, 0.3};
domain.shell_sections.push_back({"EALL", "MAT", 0.1});
domain.loads.push_back({"2", 3, -1.0});
return domain;
}
fesa::Domain noncontiguousDofDomain() {
fesa::Domain domain;
domain.nodes[30] = {30, {1, 1, 0}};
domain.nodes[10] = {10, {0, 0, 0}};
domain.nodes[40] = {40, {0, 1, 0}};
domain.nodes[20] = {20, {1, 0, 0}};
domain.elements[5] = {5, fesa::ElementType::MITC4, {10, 20, 30, 40}, "EALL"};
domain.element_sets["eall"] = {"EALL", {5}};
domain.node_sets["clamped"] = {"CLAMPED", {20}};
domain.materials["mat"] = {"MAT", 1000.0, 0.3};
domain.shell_sections.push_back({"EALL", "MAT", 0.1});
domain.boundary_conditions.push_back({"CLAMPED", 1, 6, 0.0});
domain.boundary_conditions.push_back({"30", 1, 2, 0.0});
return domain;
}
} // namespace
FESA_TEST(core_types_and_dof_mapping_are_stable) {
FESA_CHECK(sizeof(fesa::Real) == 8);
FESA_CHECK(sizeof(fesa::GlobalId) == 8);
FESA_CHECK(sizeof(fesa::LocalIndex) == 8);
FESA_CHECK(sizeof(fesa::EquationId) == 8);
FESA_CHECK(sizeof(fesa::SparseIndex) == 8);
FESA_CHECK(std::numeric_limits<fesa::GlobalId>::is_signed);
FESA_CHECK(std::numeric_limits<fesa::LocalIndex>::is_signed);
FESA_CHECK(std::numeric_limits<fesa::EquationId>::is_signed);
FESA_CHECK(std::numeric_limits<fesa::SparseIndex>::is_signed);
const auto dofs = fesa::allDofs();
FESA_CHECK(dofs.size() == 6);
for (std::size_t i = 0; i < dofs.size(); ++i) {
const int abaqus_number = static_cast<int>(i + 1);
FESA_CHECK(fesa::abaqusDofNumber(dofs[i]) == abaqus_number);
FESA_CHECK(fesa::dofFromAbaqus(abaqus_number).value() == dofs[i]);
FESA_CHECK(std::string(fesa::dofLabel(dofs[i])) == fesa::displacementComponentLabels()[i]);
}
FESA_CHECK(!fesa::dofFromAbaqus(0).has_value());
FESA_CHECK(!fesa::dofFromAbaqus(7).has_value());
}
FESA_TEST(parser_accepts_phase1_subset) {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(phase1Input());
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.nodes.size() == 4);
FESA_CHECK(parsed.domain.elements.size() == 1);
FESA_CHECK(parsed.domain.node_sets.at("left").node_ids.size() == 2);
FESA_CHECK(parsed.domain.element_sets.at("eall").element_ids.size() == 1);
FESA_CHECK(parsed.domain.materials.at("steel").elastic_modulus == 1000.0);
FESA_CHECK(parsed.domain.shell_sections.front().thickness == 0.1);
FESA_CHECK(parsed.domain.boundary_conditions.size() == 3);
FESA_CHECK(parsed.domain.loads.size() == 2);
}
FESA_TEST(parser_accepts_repeated_and_generated_sets) {
const std::string text = R"inp(
*Node
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element, type=S4, elset=EALL
1, 1, 2, 3, 4
*Nset, nset=FIXED
1, 2, 2
3
*Nset, nset=FIXED, generate
3, 4, 1
*Nset, nset=LOADS, generate
2, 4, 2
*Elset, elset=EALL
1, 1
*Elset, elset=CHECK, generate
1, 5, 2
*Material, name=MAT
*Elastic
2.0D5, 0.25
*Shell Section, elset=EALL, material=MAT
0.2
*Boundary
FIXED, 1, 6
*Cload
LOADS, 3, -2.5
*Step, name=Step-A, nlgeom=NO
*Static
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 2, 3, 4}));
FESA_CHECK(parsed.domain.node_sets.at("loads").node_ids == std::vector<fesa::GlobalId>({2, 4}));
FESA_CHECK(parsed.domain.element_sets.at("eall").element_ids == std::vector<fesa::GlobalId>({1}));
FESA_CHECK(parsed.domain.element_sets.at("check").element_ids == std::vector<fesa::GlobalId>({1, 3, 5}));
FESA_CHECK(parsed.domain.materials.at("mat").elastic_modulus == 2.0e5);
FESA_CHECK(parsed.domain.steps.front().name == "Step-A");
}
FESA_TEST(parser_accepts_keyword_line_continuation) {
const std::string text = R"inp(
*Node
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element,
type=S4, elset=EALL
1, 1, 2, 3, 4
*Nset,
nset=FIXED, generate
1, 4, 3
*Elset,
elset=EALL
1
*Material,
name=MAT
*Elastic
2.0e5, 0.25
*Shell Section,
elset=EALL, material=MAT
0.2
*Boundary
FIXED, 1, 6
*Cload
2, 3, -1.0
*Step,
name=Step-1
*Static
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.elements.at(1).source_elset == "EALL");
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 4}));
FESA_CHECK(parsed.domain.materials.count("mat") == 1);
FESA_CHECK(parsed.domain.shell_sections.front().material == "MAT");
}
FESA_TEST(parser_rejects_unsupported_features) {
const std::string text = R"inp(
*Part, name=P1
*Assembly, name=A1
*Instance, name=I1, part=P1
*Include, input=other.inp
*Node
1, 0, 0, 0
*Element, type=S4R
1, 1, 2, 3, 4
*Density
7850
*Step, nlgeom=YES
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text);
FESA_CHECK(!parsed.ok());
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD") >= 4);
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"));
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-NLGEOM"));
}
FESA_TEST(parser_rejects_unsupported_keyword_parameters_and_modes) {
const std::string text = R"inp(
*Node, input=nodes.csv
1, 0, 0, 0
2, 1, 0, 0
3, 1, 1, 0
4, 0, 1, 0
*Element, type=S4, elset=EALL, orientation=OR1
1, 1, 2, 3, 4
*Nset, nset=FIXED, unsorted
1, 4
*Material, name=MAT, description=bad
*Elastic, type=ENGINEERING CONSTANTS
2.0e5, 0.25
*Shell Section, elset=EALL, material=MAT, offset=SPOS
0.2, 5
*Boundary, op=NEW
FIXED, 1, 6
*Cload, amplitude=A1
2, 3, -1.0
*Step, name=Step-1, inc=100
*Static, stabilize
*End Step
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text, "unsupported_modes.inp");
FESA_CHECK(!parsed.ok());
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-PARAMETER") >= 8);
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-SHELL-SECTION-UNSUPPORTED"));
}
FESA_TEST(parser_diagnostics_include_file_line_and_keyword) {
const std::string text = R"inp(
*Node
1, bad, 0, 0
*Boundary
FIXED, 7, 7
)inp";
fesa::AbaqusInputParser parser;
auto parsed = parser.parseString(text, "malformed.inp");
FESA_CHECK(!parsed.ok());
const fesa::Diagnostic* node = findDiagnostic(parsed.diagnostics, "FESA-PARSE-NODE-NUMERIC");
FESA_CHECK(node != nullptr);
FESA_CHECK(node->source.file == "malformed.inp");
FESA_CHECK(node->source.line == 3);
FESA_CHECK(node->source.keyword == "node");
const fesa::Diagnostic* boundary = findDiagnostic(parsed.diagnostics, "FESA-PARSE-BOUNDARY-DOF");
FESA_CHECK(boundary != nullptr);
FESA_CHECK(boundary->source.file == "malformed.inp");
FESA_CHECK(boundary->source.line == 5);
FESA_CHECK(boundary->source.keyword == "boundary");
}
FESA_TEST(quad01_reference_input_remains_unsupported) {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_01.inp");
FESA_CHECK(!parsed.ok());
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD") ||
fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
}
FESA_TEST(quad02_original_reference_input_remains_unsupported) {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_02.inp");
FESA_CHECK(!parsed.ok());
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"));
}
FESA_TEST(quad02_phase1_normalized_input_uses_supported_subset) {
fesa::AbaqusInputParser parser;
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_02_phase1.inp");
FESA_CHECK(parsed.ok());
FESA_CHECK(parsed.domain.nodes.size() == 121);
FESA_CHECK(parsed.domain.elements.size() == 100);
FESA_CHECK(parsed.domain.node_sets.at("fixed_boundary").node_ids.size() == 40);
FESA_CHECK(parsed.domain.node_sets.at("load_node").node_ids.size() == 1);
FESA_CHECK(parsed.domain.element_sets.at("all_elements").element_ids.size() == 100);
FESA_CHECK(parsed.domain.materials.at("material_1").elastic_modulus == 7.0e10);
FESA_CHECK(parsed.domain.shell_sections.front().thickness == 1.0);
}
FESA_TEST(domain_validation_reports_missing_property_and_targets) {
fesa::Domain domain;
domain.nodes[1] = {1, {0, 0, 0}};
domain.nodes[2] = {2, {1, 0, 0}};
domain.nodes[3] = {3, {1, 1, 0}};
domain.nodes[4] = {4, {0, 1, 0}};
domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 99}, ""};
domain.shell_sections.push_back({"MISSING_ELSET", "MISSING_MAT", 0.1});
domain.loads.push_back({"MISSING", 3, 1.0});
auto diagnostics = fesa::validateDomain(domain);
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-ELEMENT-MISSING-NODE"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-PROPERTY"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-ELSET"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-MATERIAL"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET"));
for (const auto& diagnostic : diagnostics) {
FESA_CHECK(!diagnostic.code.empty());
FESA_CHECK(!diagnostic.message.empty());
FESA_CHECK(!diagnostic.source.keyword.empty());
}
}
FESA_TEST(domain_validation_reports_missing_sets_and_set_members) {
auto domain = singleElementValidationDomain();
domain.shell_sections.clear();
domain.shell_sections.push_back({"MISSING_ELSET", "MISSING_MAT", 0.1});
domain.node_sets["bad_nodes"] = {"BAD_NODES", {1, 99}};
domain.element_sets["bad_elements"] = {"BAD_ELEMENTS", {1, 77}};
domain.boundary_conditions.push_back({"MISSING_BOUNDARY_SET", 1, 6, 0.0});
domain.boundary_conditions.push_back({"BAD_NODES", 1, 1, 0.0});
domain.loads.push_back({"MISSING_LOAD_SET", 3, 1.0});
domain.loads.push_back({"BAD_NODES", 3, 1.0});
auto diagnostics = fesa::validateDomain(domain);
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-ELSET"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-MATERIAL"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-NSET-MISSING-NODE"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-ELSET-MISSING-ELEMENT"));
const auto* missing_set = findDiagnostic(diagnostics, "FESA-VALIDATION-MISSING-NSET");
FESA_CHECK(missing_set != nullptr);
FESA_CHECK(missing_set->message.find("MISSING_BOUNDARY_SET") != std::string::npos ||
missing_set->message.find("MISSING_LOAD_SET") != std::string::npos);
const auto* missing_node = findDiagnostic(diagnostics, "FESA-VALIDATION-NSET-MISSING-NODE");
FESA_CHECK(missing_node != nullptr);
FESA_CHECK(missing_node->message.find("BAD_NODES") != std::string::npos);
FESA_CHECK(missing_node->message.find("99") != std::string::npos);
}
FESA_TEST(domain_validation_reports_nonpositive_thickness_and_invalid_dofs) {
auto domain = singleElementValidationDomain();
domain.shell_sections.front().thickness = 0.0;
domain.boundary_conditions.push_back({"ALL_NODES", 0, 1, 0.0});
domain.loads.push_back({"2", 7, 1.0});
auto diagnostics = fesa::validateDomain(domain);
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-NONPOSITIVE-THICKNESS"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-BOUNDARY-DOF"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-VALIDATION-CLOAD-DOF"));
const auto* thickness = findDiagnostic(diagnostics, "FESA-VALIDATION-NONPOSITIVE-THICKNESS");
FESA_CHECK(thickness != nullptr);
FESA_CHECK(thickness->message.find("EALL") != std::string::npos);
const auto* load_dof = findDiagnostic(diagnostics, "FESA-VALIDATION-CLOAD-DOF");
FESA_CHECK(load_dof != nullptr);
FESA_CHECK(load_dof->message.find("DOF 7") != std::string::npos);
}
FESA_TEST(domain_validation_reports_no_active_elements_and_missing_load) {
fesa::Domain domain;
domain.nodes[1] = {1, {0, 0, 0}};
auto diagnostics = fesa::validateDomain(domain);
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS"));
FESA_CHECK(fesa::containsDiagnostic(diagnostics, "FESA-SINGULAR-NO-NONZERO-LOAD"));
for (const auto& diagnostic : diagnostics) {
FESA_CHECK(diagnostic.code.find("MESH") == std::string::npos);
}
}
FESA_TEST(domain_validation_reports_no_free_dofs) {
auto domain = singleElementValidationDomain();
domain.boundary_conditions.push_back({"ALL_NODES", 1, 6, 0.0});
auto diagnostics = fesa::validateDomain(domain);
const auto* no_free = findDiagnostic(diagnostics, "FESA-SINGULAR-NO-FREE-DOFS");
FESA_CHECK(no_free != nullptr);
FESA_CHECK(no_free->source.keyword == "dof");
FESA_CHECK(no_free->message.find("No free DOFs") != std::string::npos);
}
FESA_TEST(domain_validation_reports_free_untouched_dofs_for_isolated_nodes) {
auto domain = singleElementValidationDomain();
domain.nodes[99] = {99, {10, 0, 0}};
domain.boundary_conditions.push_back({"ALL_NODES", 1, 6, 0.0});
auto diagnostics = fesa::validateDomain(domain);
const auto* untouched = findDiagnostic(diagnostics, "FESA-SINGULAR-DOF-UNTOUCHED");
FESA_CHECK(untouched != nullptr);
FESA_CHECK(untouched->source.keyword == "dof");
FESA_CHECK(untouched->message.find("Node 99") != std::string::npos);
FESA_CHECK(untouched->message.find("UX") != std::string::npos);
}
FESA_TEST(domain_validation_reports_weak_drilling_dof_smoke) {
auto domain = singleElementValidationDomain();
domain.boundary_conditions.push_back({"ALL_NODES", 1, 5, 0.0});
auto diagnostics = fesa::validateDomain(domain);
const auto* weak_drilling = findDiagnostic(diagnostics, "FESA-SINGULAR-WEAK-DRILLING-DOF");
FESA_CHECK(weak_drilling != nullptr);
FESA_CHECK(weak_drilling->severity == fesa::Severity::Warning);
FESA_CHECK(weak_drilling->source.keyword == "dof");
FESA_CHECK(weak_drilling->message.find("RZ") != std::string::npos);
}
FESA_TEST(dof_manager_owns_equation_numbering_and_reconstruction) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
FESA_CHECK(dofs.fullDofCount() == 24);
FESA_CHECK(dofs.freeDofCount() == 2);
FESA_CHECK(dofs.isConstrained(1, fesa::Dof::UX));
FESA_CHECK(dofs.equation(2, fesa::Dof::UZ) == 0);
FESA_CHECK(dofs.equation(3, fesa::Dof::UZ) == 1);
auto full = dofs.reconstructFullVector({-0.1, -0.2});
FESA_CHECK_NEAR(full[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))], -0.1, 1.0e-15);
FESA_CHECK_NEAR(full[static_cast<std::size_t>(dofs.fullIndex(1, fesa::Dof::UX))], 0.0, 1.0e-15);
}
FESA_TEST(dof_manager_preserves_global_order_for_noncontiguous_nodes) {
auto domain = noncontiguousDofDomain();
fesa::DofManager dofs(domain);
FESA_CHECK(dofs.nodeIds() == std::vector<fesa::GlobalId>({10, 20, 30, 40}));
FESA_CHECK(dofs.fullDofCount() == 24);
for (std::size_t node_offset = 0; node_offset < dofs.nodeIds().size(); ++node_offset) {
const fesa::GlobalId node_id = dofs.nodeIds()[node_offset];
for (fesa::Dof dof : fesa::allDofs()) {
const fesa::LocalIndex expected = static_cast<fesa::LocalIndex>(6 * node_offset + fesa::dofIndex(dof));
FESA_CHECK(dofs.fullIndex(node_id, dof) == expected);
const auto address = dofs.fullDof(expected);
FESA_CHECK(address.node_id == node_id);
FESA_CHECK(address.dof == dof);
}
}
}
FESA_TEST(dof_manager_partitions_constraints_and_equations_deterministically) {
auto domain = noncontiguousDofDomain();
fesa::DofManager dofs(domain);
FESA_CHECK(dofs.constrainedDofCount() == 8);
FESA_CHECK(dofs.freeDofCount() == 16);
FESA_CHECK(dofs.constrainedFullIndices() == std::vector<fesa::LocalIndex>({6, 7, 8, 9, 10, 11, 12, 13}));
FESA_CHECK(dofs.freeFullIndices() ==
std::vector<fesa::LocalIndex>({0, 1, 2, 3, 4, 5, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}));
FESA_CHECK(dofs.equation(10, fesa::Dof::UX) == 0);
FESA_CHECK(dofs.equation(20, fesa::Dof::RZ) == -1);
FESA_CHECK(dofs.equation(30, fesa::Dof::UX) == -1);
FESA_CHECK(dofs.equation(30, fesa::Dof::UZ) == 6);
FESA_CHECK(dofs.equation(40, fesa::Dof::RZ) == 15);
}
FESA_TEST(dof_manager_reduces_and_reconstructs_full_vectors) {
auto domain = noncontiguousDofDomain();
fesa::DofManager dofs(domain);
std::vector<fesa::Real> full(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
for (std::size_t i = 0; i < full.size(); ++i) {
full[i] = static_cast<fesa::Real>(100 + i);
}
auto reduced = dofs.reduceFullVector(full);
FESA_CHECK(reduced.size() == static_cast<std::size_t>(dofs.freeDofCount()));
FESA_CHECK_NEAR(reduced[0], full[0], 1.0e-15);
FESA_CHECK_NEAR(reduced[6], full[14], 1.0e-15);
FESA_CHECK_NEAR(reduced.back(), full[23], 1.0e-15);
auto reconstructed = dofs.reconstructFullVector(reduced);
for (fesa::LocalIndex full_index = 0; full_index < dofs.fullDofCount(); ++full_index) {
const auto address = dofs.fullDof(full_index);
if (dofs.isConstrained(address.node_id, address.dof)) {
FESA_CHECK_NEAR(reconstructed[static_cast<std::size_t>(full_index)], 0.0, 1.0e-15);
} else {
FESA_CHECK_NEAR(reconstructed[static_cast<std::size_t>(full_index)], full[static_cast<std::size_t>(full_index)], 1.0e-15);
}
}
}
FESA_TEST(dof_manager_provides_element_sparse_connectivity_inputs) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
const auto& element = domain.elements.at(1);
auto full_indices = dofs.elementFullDofIndices(element);
auto equation_ids = dofs.elementEquationIds(element);
for (fesa::LocalIndex i = 0; i < 24; ++i) {
FESA_CHECK(full_indices[static_cast<std::size_t>(i)] == i);
}
for (fesa::LocalIndex i = 0; i < 6; ++i) {
FESA_CHECK(equation_ids[static_cast<std::size_t>(i)] == -1);
FESA_CHECK(equation_ids[static_cast<std::size_t>(18 + i)] == -1);
}
FESA_CHECK(equation_ids[static_cast<std::size_t>(6 + fesa::dofIndex(fesa::Dof::UZ))] == 0);
FESA_CHECK(equation_ids[static_cast<std::size_t>(12 + fesa::dofIndex(fesa::Dof::UZ))] == 1);
FESA_CHECK(equation_ids[static_cast<std::size_t>(6 + fesa::dofIndex(fesa::Dof::UX))] == -1);
}
FESA_TEST(full_vector_reaction_recovery_uses_full_system_quantities) {
auto domain = noncontiguousDofDomain();
fesa::DofManager dofs(domain);
fesa::DenseMatrix k_full(dofs.fullDofCount(), dofs.fullDofCount());
std::vector<fesa::Real> u_full(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
std::vector<fesa::Real> f_full(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
const fesa::LocalIndex support_ux = dofs.fullIndex(20, fesa::Dof::UX);
const fesa::LocalIndex free_uz = dofs.fullIndex(30, fesa::Dof::UZ);
k_full(support_ux, support_ux) = 10.0;
k_full(support_ux, free_uz) = 2.0;
k_full(free_uz, support_ux) = 2.0;
k_full(free_uz, free_uz) = 5.0;
u_full[static_cast<std::size_t>(free_uz)] = 3.0;
f_full[static_cast<std::size_t>(support_ux)] = 1.0;
f_full[static_cast<std::size_t>(free_uz)] = 7.0;
auto reaction = fesa::recoverFullReaction(k_full, u_full, f_full);
FESA_CHECK_NEAR(reaction[static_cast<std::size_t>(support_ux)], 5.0, 1.0e-15);
FESA_CHECK_NEAR(reaction[static_cast<std::size_t>(free_uz)], 8.0, 1.0e-15);
}
FESA_TEST(gaussian_solver_solves_and_diagnoses_singular_systems) {
fesa::DenseMatrix a(2, 2);
a(0, 0) = 2.0;
a(0, 1) = 1.0;
a(1, 0) = 1.0;
a(1, 1) = 3.0;
fesa::GaussianEliminationSolver solver;
auto solved = solver.solve(a, {1.0, 2.0});
FESA_CHECK(solved.ok());
FESA_CHECK_NEAR(solved.x[0], 0.2, 1.0e-12);
FESA_CHECK_NEAR(solved.x[1], 0.6, 1.0e-12);
fesa::DenseMatrix singular(2, 2);
singular(0, 0) = 1.0;
singular(0, 1) = 2.0;
singular(1, 0) = 2.0;
singular(1, 1) = 4.0;
auto failed = solver.solve(singular, {1.0, 2.0});
FESA_CHECK(!failed.ok());
FESA_CHECK(fesa::containsDiagnostic(failed.diagnostics, "FESA-SINGULAR-SOLVER"));
}
FESA_TEST(results_writer_uses_step_frame_fields_for_u_and_rf) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
std::vector<fesa::Real> u(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
std::vector<fesa::Real> rf(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
u[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))] = -0.1;
rf[static_cast<std::size_t>(dofs.fullIndex(1, fesa::Dof::UZ))] = 1.0;
fesa::InMemoryResultsWriter writer;
writer.writeLinearStatic(domain, dofs, u, rf);
const auto& result = writer.result();
FESA_CHECK(result.schema_name == "FESA_RESULTS");
FESA_CHECK(result.schema_version == 1);
FESA_CHECK(result.solver_name == "FESA");
FESA_CHECK(result.dof_convention == "UX,UY,UZ,RX,RY,RZ");
FESA_CHECK(result.sign_convention == "Abaqus-compatible");
FESA_CHECK(result.precision == "double");
FESA_CHECK(result.index_type == "int64");
FESA_CHECK(result.steps.size() == 1);
FESA_CHECK(result.steps[0].name == "Step-1");
FESA_CHECK(result.steps[0].frames.size() == 1);
const auto& frame = result.steps[0].frames[0];
FESA_CHECK(frame.frame_id == 0);
FESA_CHECK(frame.increment == 1);
FESA_CHECK(frame.iteration == 0);
FESA_CHECK(frame.converged);
FESA_CHECK_NEAR(frame.step_time, 1.0, 1.0e-15);
FESA_CHECK_NEAR(frame.total_time, 1.0, 1.0e-15);
FESA_CHECK(frame.field_outputs.count("U") == 1);
FESA_CHECK(frame.field_outputs.count("RF") == 1);
const auto& u_field = frame.field_outputs.at("U");
const auto& rf_field = frame.field_outputs.at("RF");
FESA_CHECK(u_field.name == "U");
FESA_CHECK(u_field.position == "NODAL");
FESA_CHECK(u_field.entity_type == "node");
FESA_CHECK(u_field.basis == "GLOBAL");
FESA_CHECK(u_field.component_labels == fesa::displacementComponentLabels());
FESA_CHECK(rf_field.name == "RF");
FESA_CHECK(rf_field.position == "NODAL");
FESA_CHECK(rf_field.entity_type == "node");
FESA_CHECK(rf_field.basis == "GLOBAL");
FESA_CHECK(rf_field.component_labels == fesa::reactionComponentLabels());
FESA_CHECK(u_field.component_labels[2] == "UZ");
FESA_CHECK(rf_field.component_labels[2] == "RFZ");
FESA_CHECK(u_field.entity_ids.size() == domain.nodes.size());
FESA_CHECK(rf_field.entity_ids.size() == domain.nodes.size());
}
FESA_TEST(displacement_csv_loader_accepts_quad01_format) {
auto table = fesa::loadDisplacementCsv(sourceRoot() + "/references/quad_01_displacements.csv");
FESA_CHECK(!fesa::hasError(table.diagnostics));
FESA_CHECK(table.rows.size() == 121);
FESA_CHECK(table.rows.count(1) == 1);
}
FESA_TEST(displacement_csv_loader_accepts_quad02_format) {
auto table = fesa::loadDisplacementCsv(sourceRoot() + "/references/quad_02_displacements.csv");
FESA_CHECK(!fesa::hasError(table.diagnostics));
FESA_CHECK(table.rows.size() == 121);
FESA_CHECK(table.rows.count(2) == 1);
FESA_CHECK(table.rows.at(2).values[2] < 0.0);
}
FESA_TEST(displacement_csv_loader_reports_required_header_errors) {
auto table = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2\n"
"1,0,0,0,0,0\n",
"missing-header.csv");
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-MISSING-COLUMN"));
}
FESA_TEST(displacement_csv_loader_reports_duplicate_node_rows) {
auto table = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
"1,0,0,0,0,0,0\n"
"1,0,0,0,0,0,0\n",
"duplicate-node.csv");
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-DUPLICATE-NODE"));
}
FESA_TEST(displacement_csv_loader_reports_missing_and_non_numeric_node_rows) {
auto table = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
",0,0,0,0,0,0\n"
"two,0,0,0,0,0,0\n"
"3,0,not-a-number,0,0,0,0\n",
"invalid-node-row.csv");
FESA_CHECK(diagnosticCount(table.diagnostics, "FESA-CSV-NODE") == 2);
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-NUMERIC"));
}
FESA_TEST(displacement_comparator_matches_by_node_id_not_row_order) {
fesa::FieldOutput actual;
actual.name = "U";
actual.position = "NODAL";
actual.entity_type = "node";
actual.basis = "GLOBAL";
actual.entity_ids = {2, 1};
actual.component_labels = fesa::displacementComponentLabels();
actual.values = {{{2, 0, 0, 0, 0, 0}}, {{1, 0, 0, 0, 0, 0}}};
fesa::CsvDisplacementTable expected;
expected.rows[1] = {1, {1, 0, 0, 0, 0, 0}};
expected.rows[2] = {2, {2, 0, 0, 0, 0, 0}};
auto compared = fesa::compareDisplacements(actual, expected, {1.0e-12, 1.0e-12, 1.0});
FESA_CHECK(compared.pass);
}
FESA_TEST(displacement_comparator_uses_absolute_and_relative_tolerances) {
fesa::FieldOutput actual;
actual.name = "U";
actual.position = "NODAL";
actual.entity_type = "node";
actual.basis = "GLOBAL";
actual.entity_ids = {10};
actual.component_labels = fesa::displacementComponentLabels();
actual.values = {{{5.0e-7, 100.0005, 0, 0, 0, 0}}};
fesa::CsvDisplacementTable expected;
expected.rows[10] = {10, {0.0, 100.0, 0, 0, 0, 0}};
auto loose = fesa::compareDisplacements(actual, expected, {1.0e-6, 1.0e-5, 1.0});
FESA_CHECK(loose.pass);
FESA_CHECK_NEAR(loose.max_abs_error, 5.0e-4, 1.0e-12);
auto strict = fesa::compareDisplacements(actual, expected, {1.0e-8, 1.0e-8, 1.0});
FESA_CHECK(!strict.pass);
FESA_CHECK(fesa::containsDiagnostic(strict.diagnostics, "FESA-COMPARE-TOLERANCE"));
}
FESA_TEST(displacement_comparator_rejects_wrong_component_labels_and_missing_nodes) {
fesa::FieldOutput actual;
actual.name = "U";
actual.position = "NODAL";
actual.entity_type = "node";
actual.basis = "GLOBAL";
actual.entity_ids = {1};
actual.component_labels = fesa::reactionComponentLabels();
actual.values = {{{0, 0, 0, 0, 0, 0}}};
fesa::CsvDisplacementTable expected;
expected.rows[2] = {2, {0, 0, 0, 0, 0, 0}};
auto compared = fesa::compareDisplacements(actual, expected, {1.0e-12, 1.0e-12, 1.0});
FESA_CHECK(!compared.pass);
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-COMPONENT-LABELS"));
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-MISSING-ACTUAL"));
}
FESA_TEST(displacement_comparator_reports_duplicate_actual_nodes) {
fesa::FieldOutput actual;
actual.name = "U";
actual.position = "NODAL";
actual.entity_type = "node";
actual.basis = "GLOBAL";
actual.entity_ids = {1, 1};
actual.component_labels = fesa::displacementComponentLabels();
actual.values = {{{0, 0, 0, 0, 0, 0}}, {{0, 0, 0, 0, 0, 0}}};
fesa::CsvDisplacementTable expected;
expected.rows[1] = {1, {0, 0, 0, 0, 0, 0}};
auto compared = fesa::compareDisplacements(actual, expected, {1.0e-12, 1.0e-12, 1.0});
FESA_CHECK(!compared.pass);
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-DUPLICATE-ACTUAL"));
}
FESA_TEST(mitc4_shape_functions_node_order_and_tying_points) {
auto center = fesa::shapeFunctions(0.0, 0.0);
const fesa::Real sum = center.n[0] + center.n[1] + center.n[2] + center.n[3];
FESA_CHECK_NEAR(sum, 1.0, 1.0e-15);
FESA_CHECK_NEAR(center.dr[0], -0.25, 1.0e-15);
FESA_CHECK_NEAR(center.dr[1], 0.25, 1.0e-15);
FESA_CHECK_NEAR(center.dr[2], 0.25, 1.0e-15);
FESA_CHECK_NEAR(center.dr[3], -0.25, 1.0e-15);
FESA_CHECK_NEAR(center.ds[0], -0.25, 1.0e-15);
FESA_CHECK_NEAR(center.ds[1], -0.25, 1.0e-15);
FESA_CHECK_NEAR(center.ds[2], 0.25, 1.0e-15);
FESA_CHECK_NEAR(center.ds[3], 0.25, 1.0e-15);
const auto node_points = fesa::mitc4NodeNaturalCoordinates();
FESA_CHECK_NEAR(node_points[0].xi, -1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[0].eta, -1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[1].xi, 1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[1].eta, -1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[2].xi, 1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[2].eta, 1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[3].xi, -1.0, 1.0e-15);
FESA_CHECK_NEAR(node_points[3].eta, 1.0, 1.0e-15);
for (std::size_t i = 0; i < 4; ++i) {
const auto corner = fesa::shapeFunctions(node_points[i].xi, node_points[i].eta);
FESA_CHECK_NEAR(corner.n[i], 1.0, 1.0e-15);
}
const auto tying_points = fesa::mitc4TyingPoints();
FESA_CHECK(tying_points[0].label == "A");
FESA_CHECK_NEAR(tying_points[0].natural.xi, 0.0, 1.0e-15);
FESA_CHECK_NEAR(tying_points[0].natural.eta, -1.0, 1.0e-15);
FESA_CHECK((tying_points[0].edge_node_indices == std::array<fesa::LocalIndex, 2>{0, 1}));
FESA_CHECK(tying_points[1].label == "B");
FESA_CHECK_NEAR(tying_points[1].natural.xi, -1.0, 1.0e-15);
FESA_CHECK_NEAR(tying_points[1].natural.eta, 0.0, 1.0e-15);
FESA_CHECK((tying_points[1].edge_node_indices == std::array<fesa::LocalIndex, 2>{0, 3}));
FESA_CHECK(tying_points[2].label == "C");
FESA_CHECK_NEAR(tying_points[2].natural.xi, 0.0, 1.0e-15);
FESA_CHECK_NEAR(tying_points[2].natural.eta, 1.0, 1.0e-15);
FESA_CHECK((tying_points[2].edge_node_indices == std::array<fesa::LocalIndex, 2>{3, 2}));
FESA_CHECK(tying_points[3].label == "D");
FESA_CHECK_NEAR(tying_points[3].natural.xi, 1.0, 1.0e-15);
FESA_CHECK_NEAR(tying_points[3].natural.eta, 0.0, 1.0e-15);
FESA_CHECK((tying_points[3].edge_node_indices == std::array<fesa::LocalIndex, 2>{1, 2}));
}
FESA_TEST(mitc4_geometry_builds_flat_directors_and_integration_basis) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
auto geometry = fesa::buildMITC4Geometry(coords, 0.2);
FESA_CHECK(geometry.ok());
checkVecNear(geometry.center_normal, {0, 0, 1}, 1.0e-14);
checkVecNear(geometry.g1_center, {0.5, 0, 0}, 1.0e-14);
checkVecNear(geometry.g2_center, {0, 0.5, 0}, 1.0e-14);
for (const auto& frame : geometry.nodal_frames) {
checkVecNear(frame.v1, {1, 0, 0}, 1.0e-14);
checkVecNear(frame.v2, {0, 1, 0}, 1.0e-14);
checkVecNear(frame.vn, {0, 0, 1}, 1.0e-14);
checkRightHandedOrthonormal(frame.v1, frame.v2, frame.vn);
}
auto basis = fesa::computeMITC4IntegrationBasis(geometry, 0.0, 0.0, 0.0);
FESA_CHECK(basis.ok());
checkVecNear(basis.g1, {0.5, 0, 0}, 1.0e-14);
checkVecNear(basis.g2, {0, 0.5, 0}, 1.0e-14);
checkVecNear(basis.g3, {0, 0, 0.1}, 1.0e-14);
checkVecNear(basis.local.e1, {1, 0, 0}, 1.0e-14);
checkVecNear(basis.local.e2, {0, 1, 0}, 1.0e-14);
checkVecNear(basis.local.e3, {0, 0, 1}, 1.0e-14);
checkRightHandedOrthonormal(basis.local.e1, basis.local.e2, basis.local.e3);
FESA_CHECK_NEAR(basis.jacobian, 0.025, 1.0e-14);
}
FESA_TEST(mitc4_geometry_uses_deterministic_director_fallback_axis) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 0, -1}, {0, 0, -1}}};
auto geometry = fesa::buildMITC4Geometry(coords, 0.1);
FESA_CHECK(geometry.ok());
checkVecNear(geometry.center_normal, {0, 1, 0}, 1.0e-14);
for (const auto& frame : geometry.nodal_frames) {
checkVecNear(frame.v1, {-1, 0, 0}, 1.0e-14);
checkVecNear(frame.v2, {0, 0, 1}, 1.0e-14);
checkVecNear(frame.vn, {0, 1, 0}, 1.0e-14);
checkRightHandedOrthonormal(frame.v1, frame.v2, frame.vn);
}
}
FESA_TEST(mitc4_geometry_reports_singular_geometry_and_thickness) {
const std::array<fesa::Vec3, 4> flat = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
auto invalid_thickness = fesa::buildMITC4Geometry(flat, 0.0);
FESA_CHECK(!invalid_thickness.ok());
FESA_CHECK(fesa::containsDiagnostic(invalid_thickness.diagnostics, "FESA-MITC4-THICKNESS"));
const std::array<fesa::Vec3, 4> collinear = {{{0, 0, 0}, {1, 0, 0}, {2, 0, 0}, {3, 0, 0}}};
auto singular = fesa::buildMITC4Geometry(collinear, 0.1);
FESA_CHECK(!singular.ok());
FESA_CHECK(fesa::containsDiagnostic(singular.diagnostics, "FESA-MITC4-SINGULAR-NORMAL"));
const std::array<fesa::Vec3, 4> collapsed_corner = {{{0, 0, 0}, {0, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
auto geometry = fesa::buildMITC4Geometry(collapsed_corner, 0.1);
FESA_CHECK(geometry.ok());
auto corner_basis = fesa::computeMITC4IntegrationBasis(geometry, -1.0, -1.0, 0.0);
FESA_CHECK(!corner_basis.ok());
FESA_CHECK(fesa::containsDiagnostic(corner_basis.diagnostics, "FESA-MITC4-SINGULAR-JACOBIAN"));
}
FESA_TEST(mitc4_rotation_transform_and_displacement_interpolation) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
const auto geometry = fesa::buildMITC4Geometry(coords, 0.2);
FESA_CHECK(geometry.ok());
const auto flat_rotation = fesa::mitc4LocalRotations(geometry.nodal_frames[0], {1.0, 2.0, 3.0});
FESA_CHECK_NEAR(flat_rotation.alpha, 1.0, 1.0e-14);
FESA_CHECK_NEAR(flat_rotation.beta, 2.0, 1.0e-14);
FESA_CHECK_NEAR(flat_rotation.gamma, 3.0, 1.0e-14);
checkVecNear(fesa::mitc4DirectorIncrement(geometry.nodal_frames[0], {0.0, 0.0, 5.0}), {0, 0, 0}, 1.0e-14);
checkVecNear(fesa::mitc4DirectorIncrement(geometry.nodal_frames[0], {0.0, 2.0, 5.0}), {2, 0, 0}, 1.0e-14);
std::array<fesa::Real, 24> dofs = zeroElementDofs();
for (std::size_t node = 0; node < 4; ++node) {
dofs[6 * node + 0] = 1.0;
dofs[6 * node + 1] = 0.5;
dofs[6 * node + 4] = 2.0;
dofs[6 * node + 5] = 99.0;
}
const auto mid = fesa::mitc4DisplacementDerivatives(geometry, dofs, 0.0, 0.0, 0.0);
FESA_CHECK(mid.ok());
checkVecNear(mid.displacement, {1.0, 0.5, 0.0}, 1.0e-14);
const auto top = fesa::mitc4DisplacementDerivatives(geometry, dofs, 0.0, 0.0, 1.0);
FESA_CHECK(top.ok());
checkVecNear(top.displacement, {1.2, 0.5, 0.0}, 1.0e-14);
const std::array<fesa::Vec3, 4> fallback_coords = {{{0, 0, 0}, {1, 0, 0}, {1, 0, -1}, {0, 0, -1}}};
const auto fallback_geometry = fesa::buildMITC4Geometry(fallback_coords, 0.1);
FESA_CHECK(fallback_geometry.ok());
const auto fallback_rotation = fesa::mitc4LocalRotations(fallback_geometry.nodal_frames[0], {2.0, 3.0, 5.0});
FESA_CHECK_NEAR(fallback_rotation.alpha, -2.0, 1.0e-14);
FESA_CHECK_NEAR(fallback_rotation.beta, 5.0, 1.0e-14);
FESA_CHECK_NEAR(fallback_rotation.gamma, 3.0, 1.0e-14);
}
FESA_TEST(mitc4_direct_covariant_strain_rows_match_finite_difference) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
const auto geometry = fesa::buildMITC4Geometry(coords, 0.2);
FESA_CHECK(geometry.ok());
constexpr fesa::Real xi = 0.2;
constexpr fesa::Real eta = -0.3;
constexpr fesa::Real zeta = 0.4;
const auto rows = fesa::mitc4DirectCovariantStrainRows(geometry, xi, eta, zeta);
FESA_CHECK(rows.ok());
FESA_CHECK(fesa::mitc4StrainComponentLabels()[0] == "eps11");
FESA_CHECK(fesa::mitc4StrainComponentLabels()[3] == "gamma23");
const std::array<std::size_t, 7> checked_dofs = {0, 1, 2, 7, 9, 10, 11};
const fesa::Real h = 1.0e-6;
for (std::size_t dof : checked_dofs) {
auto plus = zeroElementDofs();
auto minus = zeroElementDofs();
plus[dof] = h;
minus[dof] = -h;
const auto plus_strain = fesa::mitc4DirectCovariantStrain(geometry, plus, xi, eta, zeta);
const auto minus_strain = fesa::mitc4DirectCovariantStrain(geometry, minus, xi, eta, zeta);
FESA_CHECK(plus_strain.ok());
FESA_CHECK(minus_strain.ok());
for (std::size_t component = 0; component < 6; ++component) {
const fesa::Real finite_difference = (plus_strain.values[component] - minus_strain.values[component]) / (2.0 * h);
FESA_CHECK_NEAR(rows.rows[component][dof], finite_difference, 1.0e-8);
}
}
const auto gamma13 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma13);
const auto gamma23 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma23);
for (std::size_t node = 0; node < 4; ++node) {
const std::size_t drilling_dof = 6 * node + 5;
for (std::size_t component = 0; component < 6; ++component) {
FESA_CHECK_NEAR(rows.rows[component][drilling_dof], 0.0, 1.0e-14);
}
}
FESA_CHECK(gamma13 == 4);
FESA_CHECK(gamma23 == 3);
}
FESA_TEST(mitc4_mitc_tying_rows_use_fesa_ac_bd_signs) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
const auto geometry = fesa::buildMITC4Geometry(coords, 0.2);
FESA_CHECK(geometry.ok());
const auto gamma23 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma23);
const auto gamma13 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma13);
const auto direct_a = fesa::mitc4DirectCovariantStrainRows(geometry, 0.0, -1.0, 0.0);
const auto direct_b = fesa::mitc4DirectCovariantStrainRows(geometry, -1.0, 0.0, 0.0);
const auto direct_c = fesa::mitc4DirectCovariantStrainRows(geometry, 0.0, 1.0, 0.0);
const auto direct_d = fesa::mitc4DirectCovariantStrainRows(geometry, 1.0, 0.0, 0.0);
const auto mitc_a = fesa::mitc4TiedCovariantStrainRows(geometry, 0.0, -1.0, 0.0);
const auto mitc_b = fesa::mitc4TiedCovariantStrainRows(geometry, -1.0, 0.0, 0.0);
const auto mitc_c = fesa::mitc4TiedCovariantStrainRows(geometry, 0.0, 1.0, 0.0);
const auto mitc_d = fesa::mitc4TiedCovariantStrainRows(geometry, 1.0, 0.0, 0.0);
FESA_CHECK(direct_a.ok());
FESA_CHECK(direct_b.ok());
FESA_CHECK(direct_c.ok());
FESA_CHECK(direct_d.ok());
FESA_CHECK(mitc_a.ok());
FESA_CHECK(mitc_b.ok());
FESA_CHECK(mitc_c.ok());
FESA_CHECK(mitc_d.ok());
for (std::size_t dof = 0; dof < 24; ++dof) {
FESA_CHECK_NEAR(mitc_a.rows[gamma13][dof], direct_a.rows[gamma13][dof], 1.0e-14);
FESA_CHECK_NEAR(mitc_c.rows[gamma13][dof], direct_c.rows[gamma13][dof], 1.0e-14);
FESA_CHECK_NEAR(mitc_b.rows[gamma23][dof], direct_b.rows[gamma23][dof], 1.0e-14);
FESA_CHECK_NEAR(mitc_d.rows[gamma23][dof], direct_d.rows[gamma23][dof], 1.0e-14);
}
}
FESA_TEST(mitc4_mitc_gauss_shear_rows_are_interpolated_from_tying_rows) {
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
const auto geometry = fesa::buildMITC4Geometry(coords, 0.2);
FESA_CHECK(geometry.ok());
constexpr fesa::Real xi = 0.5;
constexpr fesa::Real eta = 0.25;
constexpr fesa::Real zeta = 0.0;
const auto gamma23 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma23);
const auto gamma13 = fesa::strainComponentIndex(fesa::MITC4StrainComponent::Gamma13);
const auto direct_at_point = fesa::mitc4DirectCovariantStrainRows(geometry, xi, eta, zeta);
const auto direct_a = fesa::mitc4DirectCovariantStrainRows(geometry, 0.0, -1.0, zeta);
const auto direct_b = fesa::mitc4DirectCovariantStrainRows(geometry, -1.0, 0.0, zeta);
const auto direct_c = fesa::mitc4DirectCovariantStrainRows(geometry, 0.0, 1.0, zeta);
const auto direct_d = fesa::mitc4DirectCovariantStrainRows(geometry, 1.0, 0.0, zeta);
const auto tied = fesa::mitc4TiedCovariantStrainRows(geometry, xi, eta, zeta);
FESA_CHECK(direct_at_point.ok());
FESA_CHECK(tied.ok());
const std::size_t node2_ry = 6 + 4;
const fesa::Real expected_gamma13 =
0.5 * (1.0 - eta) * direct_a.rows[gamma13][node2_ry] + 0.5 * (1.0 + eta) * direct_c.rows[gamma13][node2_ry];
FESA_CHECK_NEAR(tied.rows[gamma13][node2_ry], expected_gamma13, 1.0e-14);
FESA_CHECK(std::fabs(tied.rows[gamma13][node2_ry] - direct_at_point.rows[gamma13][node2_ry]) > 1.0e-4);
const std::size_t node4_rx = 3 * 6 + 3;
const fesa::Real expected_gamma23 =
0.5 * (1.0 - xi) * direct_b.rows[gamma23][node4_rx] + 0.5 * (1.0 + xi) * direct_d.rows[gamma23][node4_rx];
FESA_CHECK_NEAR(tied.rows[gamma23][node4_rx], expected_gamma23, 1.0e-14);
FESA_CHECK(std::fabs(tied.rows[gamma23][node4_rx] - direct_at_point.rows[gamma23][node4_rx]) > 1.0e-4);
for (std::size_t component : {std::size_t{0}, std::size_t{1}, std::size_t{2}, std::size_t{5}}) {
for (std::size_t dof = 0; dof < 24; ++dof) {
FESA_CHECK_NEAR(tied.rows[component][dof], direct_at_point.rows[component][dof], 1.0e-14);
}
}
}
FESA_TEST(mitc4_shape_functions_and_stiffness_baseline) {
auto shape = fesa::shapeFunctions(0.25, -0.5);
const fesa::Real sum = shape.n[0] + shape.n[1] + shape.n[2] + shape.n[3];
FESA_CHECK_NEAR(sum, 1.0, 1.0e-15);
const std::array<fesa::Vec3, 4> coords = {{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}}};
fesa::MITC4ElementKernel kernel;
auto k = kernel.stiffness(coords, 1000.0, 0.3, 0.1);
FESA_CHECK(k.rows() == 24);
FESA_CHECK(k.cols() == 24);
for (fesa::LocalIndex i = 0; i < 24; ++i) {
for (fesa::LocalIndex j = 0; j < 24; ++j) {
FESA_CHECK_NEAR(k(i, j), k(j, i), 1.0e-8);
}
}
std::vector<fesa::Real> uniform_translation(24, 0.0);
for (int node = 0; node < 4; ++node) {
uniform_translation[static_cast<std::size_t>(6 * node + 0)] = 1.0;
}
auto internal = k.multiply(uniform_translation);
fesa::Real norm = 0.0;
for (auto value : internal) {
norm += std::fabs(value);
}
FESA_CHECK(norm < 1.0e-8);
}
FESA_TEST(linear_static_analysis_solves_u_and_recovers_full_vector_rf) {
auto domain = parsedPhase1Domain();
fesa::LinearStaticAnalysis analysis;
auto result = analysis.run(domain);
FESA_CHECK(result.ok());
FESA_CHECK(result.state.converged);
FESA_CHECK(result.result_file.steps.size() == 1);
const auto& frame = result.result_file.steps[0].frames[0];
FESA_CHECK(frame.field_outputs.count("U") == 1);
FESA_CHECK(frame.field_outputs.count("RF") == 1);
fesa::Real total_rf_z = 0.0;
fesa::DofManager dofs(domain);
for (auto node_id : dofs.nodeIds()) {
total_rf_z += result.state.reaction_full[static_cast<std::size_t>(dofs.fullIndex(node_id, fesa::Dof::UZ))];
}
FESA_CHECK_NEAR(total_rf_z, 2.0, 1.0e-8);
}
int main() {
int failed = 0;
for (const auto& test : registry()) {
try {
test.fn();
std::cout << "[PASS] " << test.name << '\n';
} catch (const std::exception& error) {
++failed;
std::cerr << "[FAIL] " << test.name << ": " << error.what() << '\n';
}
}
if (failed != 0) {
std::cerr << failed << " test(s) failed\n";
return 1;
}
std::cout << registry().size() << " test(s) passed\n";
return 0;
}