refactor: extract assembly analysis workflow
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
#include "fesa/Analysis/Analysis.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool value, const char* message) {
|
||||
if (!value) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
void checkNear(fesa::Real actual, fesa::Real expected, fesa::Real tolerance, const char* message) {
|
||||
if (std::fabs(actual - expected) > tolerance) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const auto workflow = fesa::runLinearStaticInputString(phase1Input(), "analysis-module.inp");
|
||||
check(workflow.ok(), "linear static input workflow should remain valid");
|
||||
check(workflow.model.ok(), "analysis model should remain valid");
|
||||
check(workflow.model.step.name == "Step-1", "analysis step name changed");
|
||||
check(workflow.state.converged, "analysis convergence flag changed");
|
||||
check(workflow.state.u_full.size() == 24, "full displacement size changed");
|
||||
check(workflow.state.f_external_full.size() == 24, "full external-force size changed");
|
||||
check(workflow.state.f_internal_full.size() == 24, "full internal-force size changed");
|
||||
check(workflow.state.reaction_full.size() == 24, "full reaction size changed");
|
||||
check(workflow.result_file.steps.size() == 1, "result step count changed");
|
||||
const auto& frame = workflow.result_file.steps.front().frames.front();
|
||||
check(frame.field_outputs.count("U") == 1, "U field output missing");
|
||||
check(frame.field_outputs.count("RF") == 1, "RF field output missing");
|
||||
check(frame.field_outputs.at("U").component_labels == fesa::displacementComponentLabels(), "U labels changed");
|
||||
check(frame.field_outputs.at("RF").component_labels == fesa::reactionComponentLabels(), "RF labels changed");
|
||||
|
||||
fesa::Real total_rf_z = 0.0;
|
||||
for (const auto& values : frame.field_outputs.at("RF").values) {
|
||||
total_rf_z += values[2];
|
||||
}
|
||||
checkNear(total_rf_z, 2.0, 1.0e-8, "full-vector RF balance changed");
|
||||
|
||||
fesa::AbaqusInputParser parser;
|
||||
const auto parsed = parser.parseString(phase1Input());
|
||||
check(parsed.ok(), "phase1 analysis input parse changed");
|
||||
RecordingSolver solver({0.25, -0.50});
|
||||
fesa::LinearStaticAnalysis analysis(&solver);
|
||||
const auto injected = analysis.run(parsed.domain);
|
||||
check(injected.ok(), "solver-injected analysis should remain valid");
|
||||
check(solver.called, "linear solver adapter injection changed");
|
||||
check(solver.captured_a.rows() == 2 && solver.captured_a.cols() == 2, "captured reduced stiffness size changed");
|
||||
check(solver.captured_b.size() == 2, "captured reduced load size changed");
|
||||
|
||||
const fesa::DofManager dofs(parsed.domain);
|
||||
checkNear(injected.state.u_full[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))], 0.25, 1.0e-15,
|
||||
"node 2 reconstructed UZ changed");
|
||||
checkNear(injected.state.u_full[static_cast<std::size_t>(dofs.fullIndex(3, fesa::Dof::UZ))], -0.50, 1.0e-15,
|
||||
"node 3 reconstructed UZ changed");
|
||||
for (std::size_t i = 0; i < injected.state.reaction_full.size(); ++i) {
|
||||
checkNear(injected.state.reaction_full[i],
|
||||
injected.state.f_internal_full[i] - injected.state.f_external_full[i],
|
||||
1.0e-10,
|
||||
"full-vector reaction formula changed");
|
||||
}
|
||||
|
||||
const auto parse_error = fesa::runLinearStaticInputString("*Part, name=P\n", "unsupported.inp");
|
||||
check(!parse_error.ok(), "parse errors should still be routed through analysis result");
|
||||
check(fesa::containsDiagnostic(parse_error.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"),
|
||||
"parse diagnostic routing changed");
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "fesa/Assembly/Assembly.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool value, const char* message) {
|
||||
if (!value) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
void checkNear(fesa::Real actual, fesa::Real expected, fesa::Real tolerance, const char* message) {
|
||||
if (std::fabs(actual - expected) > tolerance) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
fesa::Domain assemblyDomain() {
|
||||
fesa::Domain domain;
|
||||
domain.nodes[1] = {1, {0.0, 0.0, 0.0}};
|
||||
domain.nodes[2] = {2, {1.0, 0.0, 0.0}};
|
||||
domain.nodes[3] = {3, {1.0, 1.0, 0.0}};
|
||||
domain.nodes[4] = {4, {0.0, 1.0, 0.0}};
|
||||
domain.elements[1] = {1, fesa::ElementType::MITC4, {1, 2, 3, 4}, "EALL"};
|
||||
domain.element_sets["eall"] = {"EALL", {1}};
|
||||
domain.node_sets["left"] = {"LEFT", {1, 4}};
|
||||
domain.node_sets["right"] = {"RIGHT", {2, 3}};
|
||||
domain.materials["steel"] = {"STEEL", 1000.0, 0.30};
|
||||
domain.shell_sections.push_back({"EALL", "STEEL", 0.1});
|
||||
domain.boundary_conditions.push_back({"LEFT", 1, 6, 0.0});
|
||||
domain.boundary_conditions.push_back({"RIGHT", 1, 2, 0.0});
|
||||
domain.boundary_conditions.push_back({"RIGHT", 4, 6, 0.0});
|
||||
domain.loads.push_back({"2", 3, -1.0});
|
||||
domain.loads.push_back({"3", 3, -1.0});
|
||||
return domain;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const auto domain = assemblyDomain();
|
||||
const fesa::DofManager dofs(domain);
|
||||
|
||||
const auto pattern = fesa::buildReducedSparsePattern(domain, dofs);
|
||||
check(pattern.equation_count == dofs.freeDofCount(), "reduced sparse pattern equation count changed");
|
||||
check(pattern.nonzeroCount() > 0, "reduced sparse pattern should remain non-empty");
|
||||
|
||||
const auto assembly = fesa::assembleSystem(domain, dofs);
|
||||
check(assembly.ok(), "assembly should remain valid");
|
||||
check(assembly.k_full.rows() == dofs.fullDofCount(), "full stiffness row count changed");
|
||||
check(assembly.k_full.cols() == dofs.fullDofCount(), "full stiffness column count changed");
|
||||
check(static_cast<fesa::LocalIndex>(assembly.f_full.size()) == dofs.fullDofCount(), "full load size changed");
|
||||
checkNear(assembly.f_full[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))], -1.0, 1.0e-15,
|
||||
"node 2 UZ load changed");
|
||||
checkNear(assembly.f_full[static_cast<std::size_t>(dofs.fullIndex(3, fesa::Dof::UZ))], -1.0, 1.0e-15,
|
||||
"node 3 UZ load changed");
|
||||
|
||||
const auto reduced = fesa::projectToReducedSystem(assembly, dofs);
|
||||
check(reduced.ok(), "reduced system projection should remain valid");
|
||||
check(reduced.k.rows() == dofs.freeDofCount(), "reduced stiffness row count changed");
|
||||
check(reduced.k.cols() == dofs.freeDofCount(), "reduced stiffness column count changed");
|
||||
check(static_cast<fesa::LocalIndex>(reduced.f.size()) == dofs.freeDofCount(), "reduced load size changed");
|
||||
check(reduced.free_full_indices == dofs.freeFullIndices(), "free full-index map changed");
|
||||
checkNear(reduced.f[0], -1.0, 1.0e-15, "first reduced load changed");
|
||||
checkNear(reduced.f[1], -1.0, 1.0e-15, "second reduced load changed");
|
||||
|
||||
fesa::DenseMatrix k(3, 3);
|
||||
k(0, 0) = 4.0;
|
||||
k(1, 1) = 5.0;
|
||||
k(2, 2) = 6.0;
|
||||
const std::vector<fesa::Real> u = {0.5, -0.25, 2.0};
|
||||
const std::vector<fesa::Real> f = {1.0, 2.0, -3.0};
|
||||
const auto rf = fesa::recoverFullReaction(k, u, f);
|
||||
check(rf.size() == 3, "full reaction size changed");
|
||||
checkNear(rf[0], 1.0, 1.0e-15, "RF component 0 changed");
|
||||
checkNear(rf[1], -3.25, 1.0e-15, "RF component 1 changed");
|
||||
checkNear(rf[2], 15.0, 1.0e-15, "RF component 2 changed");
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user