feat: add assembly reduced solver boundary
This commit is contained in:
@@ -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()) {
|
||||
|
||||
Reference in New Issue
Block a user