feat: add assembly reduced solver boundary

This commit is contained in:
NINI
2026-05-04 23:35:41 +09:00
parent ebdb2519dc
commit d373969732
5 changed files with 328 additions and 22 deletions
+153
View File
@@ -230,6 +230,33 @@ fesa::Real singleElementCantileverTipUz(fesa::Real thickness) {
return 0.5 * (node2_uz + node3_uz);
}
class RecordingSolver final : public fesa::LinearSolver {
public:
explicit RecordingSolver(std::vector<fesa::Real> solution) : solution_(std::move(solution)) {}
fesa::SolveResult solve(fesa::DenseMatrix a, std::vector<fesa::Real> b) const override {
called = true;
captured_a = std::move(a);
captured_b = std::move(b);
return {solution_, {}};
}
mutable bool called = false;
mutable fesa::DenseMatrix captured_a;
mutable std::vector<fesa::Real> captured_b;
private:
std::vector<fesa::Real> solution_;
};
class FailingSolver final : public fesa::LinearSolver {
public:
fesa::SolveResult solve(fesa::DenseMatrix, std::vector<fesa::Real>) const override {
return {{}, {fesa::makeDiagnostic(fesa::Severity::Error, "FESA-SINGULAR-SOLVER",
"Injected reduced system singularity", "solver")}};
}
};
fesa::Domain singleElementValidationDomain() {
fesa::Domain domain;
domain.nodes[1] = {1, {0, 0, 0}};
@@ -725,6 +752,96 @@ FESA_TEST(full_vector_reaction_recovery_uses_full_system_quantities) {
FESA_CHECK_NEAR(reaction[static_cast<std::size_t>(free_uz)], 8.0, 1.0e-15);
}
FESA_TEST(reduced_sparse_pattern_is_deterministic_for_phase1_connectivity) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
const auto pattern = fesa::buildReducedSparsePattern(domain, dofs);
FESA_CHECK(pattern.equation_count == dofs.freeDofCount());
FESA_CHECK(pattern.nonzeroCount() == 4);
FESA_CHECK(pattern.contains(0, 0));
FESA_CHECK(pattern.contains(0, 1));
FESA_CHECK(pattern.contains(1, 0));
FESA_CHECK(pattern.contains(1, 1));
FESA_CHECK(pattern.entries[0].row == 0);
FESA_CHECK(pattern.entries[0].col == 0);
FESA_CHECK(pattern.entries[1].row == 0);
FESA_CHECK(pattern.entries[1].col == 1);
FESA_CHECK(pattern.entries[2].row == 1);
FESA_CHECK(pattern.entries[2].col == 0);
FESA_CHECK(pattern.entries[3].row == 1);
FESA_CHECK(pattern.entries[3].col == 1);
}
FESA_TEST(assembly_projection_uses_dof_manager_free_indices) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
fesa::AssemblyResult assembly;
assembly.k_full = fesa::DenseMatrix(dofs.fullDofCount(), dofs.fullDofCount());
assembly.f_full = std::vector<fesa::Real>(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
assembly.reduced_pattern = fesa::buildReducedSparsePattern(domain, dofs);
const auto node2_uz = dofs.fullIndex(2, fesa::Dof::UZ);
const auto node3_uz = dofs.fullIndex(3, fesa::Dof::UZ);
const auto support_uz = dofs.fullIndex(1, fesa::Dof::UZ);
assembly.k_full(node2_uz, node2_uz) = 3.0;
assembly.k_full(node2_uz, node3_uz) = 1.0;
assembly.k_full(node3_uz, node2_uz) = 1.0;
assembly.k_full(node3_uz, node3_uz) = 2.0;
assembly.k_full(support_uz, node2_uz) = 7.0;
assembly.f_full[static_cast<std::size_t>(node2_uz)] = -1.0;
assembly.f_full[static_cast<std::size_t>(node3_uz)] = -2.0;
const auto reduced = fesa::projectToReducedSystem(assembly, dofs);
FESA_CHECK(reduced.ok());
FESA_CHECK(reduced.k.rows() == 2);
FESA_CHECK(reduced.k.cols() == 2);
FESA_CHECK(reduced.free_full_indices == dofs.freeFullIndices());
FESA_CHECK_NEAR(reduced.k(0, 0), 3.0, 1.0e-15);
FESA_CHECK_NEAR(reduced.k(0, 1), 1.0, 1.0e-15);
FESA_CHECK_NEAR(reduced.k(1, 0), 1.0, 1.0e-15);
FESA_CHECK_NEAR(reduced.k(1, 1), 2.0, 1.0e-15);
FESA_CHECK_NEAR(reduced.f[0], -1.0, 1.0e-15);
FESA_CHECK_NEAR(reduced.f[1], -2.0, 1.0e-15);
fesa::GaussianEliminationSolver solver;
const auto solved = solver.solve(reduced.k, reduced.f);
FESA_CHECK(solved.ok());
FESA_CHECK_NEAR(solved.x[0], 0.0, 1.0e-12);
FESA_CHECK_NEAR(solved.x[1], -1.0, 1.0e-12);
}
FESA_TEST(assembly_preserves_full_space_stiffness_load_and_reduced_pattern) {
auto domain = parsedPhase1Domain();
fesa::DofManager dofs(domain);
const auto assembly = fesa::assembleSystem(domain, dofs);
FESA_CHECK(!fesa::hasError(assembly.diagnostics));
FESA_CHECK(assembly.k_full.rows() == dofs.fullDofCount());
FESA_CHECK(assembly.k_full.cols() == dofs.fullDofCount());
FESA_CHECK(assembly.f_full.size() == static_cast<std::size_t>(dofs.fullDofCount()));
FESA_CHECK(assembly.reduced_pattern.equation_count == dofs.freeDofCount());
FESA_CHECK(assembly.reduced_pattern.nonzeroCount() == 4);
const auto node2_uz = dofs.fullIndex(2, fesa::Dof::UZ);
const auto node3_uz = dofs.fullIndex(3, fesa::Dof::UZ);
FESA_CHECK_NEAR(assembly.f_full[static_cast<std::size_t>(node2_uz)], -1.0, 1.0e-15);
FESA_CHECK_NEAR(assembly.f_full[static_cast<std::size_t>(node3_uz)], -1.0, 1.0e-15);
for (fesa::LocalIndex i = 0; i < assembly.k_full.rows(); ++i) {
for (fesa::LocalIndex j = 0; j < assembly.k_full.cols(); ++j) {
FESA_CHECK_NEAR(assembly.k_full(i, j), assembly.k_full(j, i), 1.0e-8);
}
}
const auto reduced = fesa::projectToReducedSystem(assembly, dofs);
FESA_CHECK(reduced.ok());
const auto solved = fesa::GaussianEliminationSolver{}.solve(reduced.k, reduced.f);
FESA_CHECK(solved.ok());
const auto residual = reduced.k.multiply(solved.x);
for (std::size_t i = 0; i < residual.size(); ++i) {
FESA_CHECK_NEAR(residual[i], reduced.f[i], 1.0e-8);
}
}
FESA_TEST(gaussian_solver_solves_and_diagnoses_singular_systems) {
fesa::DenseMatrix a(2, 2);
a(0, 0) = 2.0;
@@ -1573,6 +1690,9 @@ FESA_TEST(linear_static_analysis_solves_u_and_recovers_full_vector_rf) {
auto result = analysis.run(domain);
FESA_CHECK(result.ok());
FESA_CHECK(result.state.converged);
FESA_CHECK(result.state.f_internal_full.size() == result.state.u_full.size());
FESA_CHECK(result.state.f_external_full.size() == result.state.u_full.size());
FESA_CHECK(result.state.reaction_full.size() == result.state.u_full.size());
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);
@@ -1585,6 +1705,39 @@ FESA_TEST(linear_static_analysis_solves_u_and_recovers_full_vector_rf) {
FESA_CHECK_NEAR(total_rf_z, 2.0, 1.0e-8);
}
FESA_TEST(linear_static_analysis_uses_solver_adapter_and_reconstructs_full_vectors) {
auto domain = parsedPhase1Domain();
RecordingSolver solver({0.25, -0.50});
fesa::LinearStaticAnalysis analysis(&solver);
const auto result = analysis.run(domain);
FESA_CHECK(result.ok());
FESA_CHECK(solver.called);
FESA_CHECK(solver.captured_a.rows() == 2);
FESA_CHECK(solver.captured_a.cols() == 2);
FESA_CHECK(solver.captured_b.size() == 2);
FESA_CHECK_NEAR(solver.captured_b[0], -1.0, 1.0e-15);
FESA_CHECK_NEAR(solver.captured_b[1], -1.0, 1.0e-15);
fesa::DofManager dofs(domain);
FESA_CHECK_NEAR(result.state.u_full[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))], 0.25, 1.0e-15);
FESA_CHECK_NEAR(result.state.u_full[static_cast<std::size_t>(dofs.fullIndex(3, fesa::Dof::UZ))], -0.50, 1.0e-15);
FESA_CHECK_NEAR(result.state.u_full[static_cast<std::size_t>(dofs.fullIndex(1, fesa::Dof::UZ))], 0.0, 1.0e-15);
for (std::size_t i = 0; i < result.state.reaction_full.size(); ++i) {
FESA_CHECK_NEAR(result.state.reaction_full[i],
result.state.f_internal_full[i] - result.state.f_external_full[i], 1.0e-10);
}
}
FESA_TEST(linear_static_analysis_propagates_solver_singular_diagnostic) {
auto domain = parsedPhase1Domain();
FailingSolver solver;
fesa::LinearStaticAnalysis analysis(&solver);
const auto result = analysis.run(domain);
FESA_CHECK(!result.ok());
FESA_CHECK(!result.state.converged);
FESA_CHECK(fesa::containsDiagnostic(result.diagnostics, "FESA-SINGULAR-SOLVER"));
}
int main() {
int failed = 0;
for (const auto& test : registry()) {