#include "fesa/analysis/linear_static_analysis.hpp" #include "fesa/assembly/parallel_for.hpp" #include "fesa/results/results_writer.hpp" #include "fesa/solvers/linear/mkl_pardiso_solver.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace { class TempDirectory { public: explicit TempDirectory(const std::string& label) { static std::atomic sequence{0U}; const auto tick = std::chrono::steady_clock::now() .time_since_epoch() .count(); path_ = std::filesystem::temp_directory_path() / ("fesa-step24-analysis-" + label + "-" + std::to_string(tick) + "-" + std::to_string(sequence.fetch_add(1U))); std::error_code error; if (!std::filesystem::create_directory(path_, error) || error) { throw std::runtime_error{"Unable to create the Step 24 analysis fixture."}; } } TempDirectory(const TempDirectory&) = delete; TempDirectory& operator=(const TempDirectory&) = delete; ~TempDirectory() { std::error_code ignored; std::filesystem::remove_all(path_, ignored); } const std::filesystem::path& path() const noexcept { return path_; } private: std::filesystem::path path_; }; void writeText(const std::filesystem::path& path, const std::string& text) { std::ofstream stream{path, std::ios::binary | std::ios::trunc}; stream.write(text.data(), static_cast(text.size())); if (!stream) { throw std::runtime_error{"Unable to write the Step 24 analysis input."}; } } std::string axialDeck(const double rootUx, const double tipForce) { return R"inp(*Part, name=BeamPart *Node 1, 0., 0., 0. 2, 2., 0., 0. *Element, type=B33 1, 1, 2 *Elset, elset=BeamSet 1 *Beam General Section, elset=BeamSet, material=Steel, section=GENERAL 2., 0.5, 0., 0.75, 0.25 0., 1., 0. *End Part *Assembly, name=Assembly *Instance, name=Beam-1, part=BeamPart *End Instance *Nset, nset=Root, instance=Beam-1 1 *Nset, nset=Tip, instance=Beam-1 2 *End Assembly *Material, name=Steel *Elastic 100., 0.25 *Boundary Root, 1, 1, )inp" + std::to_string(rootUx) + R"inp( Root, 2, 6 Tip, 2, 6 *Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. *Cload Tip, 1, )inp" + std::to_string(tipForce) + R"inp( *End Step )inp"; } // The pure Template Method spy makes the eight public lifecycle hooks observable // without coupling the ordering assertion to any solver backend. class SpyAnalysis final : public fesa::Analysis { public: const std::vector& events() const noexcept { return events_; } protected: fesa::Status initialize(const fesa::AnalysisRequest&) override { return record("initialize"); } fesa::Status buildAnalysisModel() override { return record("build-analysis-model"); } fesa::Status buildDofMapAndSparsePattern() override { return record("build-dof-map-and-sparse-pattern"); } fesa::Status assembleAndPartitionStiffness() override { return record("assemble-and-partition-stiffness"); } fesa::Status factorize() override { return record("factorize"); } fesa::Status assembleLoadsAndEffectiveRhs() override { return record("assemble-loads-and-effective-rhs"); } fesa::Status substituteAndReconstruct() override { return record("substitute-and-reconstruct"); } fesa::Status recoverAndWriteResults() override { return record("recover-and-write-results"); } private: fesa::Status record(const char* event) { events_.emplace_back(event); return fesa::Status::ok(); } std::vector events_; }; // The solver spy records only the adapter-boundary operations. In particular, // solve() cannot conceal a second factorization call. class SpyLinearSolver final : public fesa::LinearSolver { public: explicit SpyLinearSolver(std::vector& events) : events_{events} {} fesa::Status factorize(const fesa::SparseMatrix&) override { ++factorizeCalls_; events_.emplace_back("solver-factorize"); return fesa::Status::ok(); } fesa::Status solve( const fesa::Vector& rhs, fesa::Vector& solution) const override { ++solveCalls_; events_.emplace_back("solver-solve"); for (std::size_t index = 0U; index < rhs.size() && index < solution.size(); ++index) { solution[index] = 0.0; } return fesa::Status::ok(); } int factorizeCalls() const noexcept { return factorizeCalls_; } int solveCalls() const noexcept { return solveCalls_; } private: std::vector& events_; int factorizeCalls_{0}; mutable int solveCalls_{0}; }; class SpyResultsWriter final : public fesa::ResultsWriter { public: explicit SpyResultsWriter(std::vector& events) : events_{events} {} fesa::Status write( const std::filesystem::path&, const fesa::Domain&, const fesa::AnalysisState&, const std::vector&) override { ++writeCalls_; events_.emplace_back("writer-write"); return fesa::Status::ok(); } int writeCalls() const noexcept { return writeCalls_; } private: std::vector& events_; int writeCalls_{0}; }; class CapturingResultsWriter final : public fesa::ResultsWriter { public: fesa::Status write( const std::filesystem::path& outputPath, const fesa::Domain& domain, const fesa::AnalysisState& state, const std::vector& diagnostics) override { outputPath_ = outputPath; nodeCount_ = domain.nodes().size(); state_ = std::make_unique(state); diagnostics_ = diagnostics; return fesa::Status::ok(); } const fesa::AnalysisState& state() const { if (!state_) { throw std::logic_error{"No AnalysisState was captured."}; } return *state_; } const std::filesystem::path& outputPath() const noexcept { return outputPath_; } std::size_t nodeCount() const noexcept { return nodeCount_; } const std::vector& diagnostics() const noexcept { return diagnostics_; } private: std::filesystem::path outputPath_; std::size_t nodeCount_{0U}; std::unique_ptr state_; std::vector diagnostics_; }; } // namespace TEST(LinearStaticCli, FactorizesBeforeLoadAndSolvesWithoutRefactorization) { SpyAnalysis lifecycle; const fesa::AnalysisRequest emptyRequest{}; ASSERT_TRUE(lifecycle.run(emptyRequest).isOk()); EXPECT_EQ( lifecycle.events(), (std::vector{ "initialize", "build-analysis-model", "build-dof-map-and-sparse-pattern", "assemble-and-partition-stiffness", "factorize", "assemble-loads-and-effective-rhs", "substitute-and-reconstruct", "recover-and-write-results"})); TempDirectory directory{"order"}; const auto input = directory.path() / "order.inp"; const auto output = directory.path() / "results.h5"; writeText(input, axialDeck(0.0, 0.0)); std::vector adapterEvents; fesa::SerialParallelFor serial; SpyLinearSolver solver{adapterEvents}; SpyResultsWriter writer{adapterEvents}; fesa::LinearStaticAnalysis analysis{serial, solver, writer}; ASSERT_TRUE(analysis.run({input, output}).isOk()); EXPECT_EQ(solver.factorizeCalls(), 1); EXPECT_EQ(solver.solveCalls(), 1); EXPECT_EQ(writer.writeCalls(), 1); EXPECT_EQ( adapterEvents, (std::vector{ "solver-factorize", "solver-solve", "writer-write"})); } TEST(LinearStaticCli, RealPipelineHandlesAnalyticalAndNonzeroPrescription) { TempDirectory directory{"analytical"}; const auto input = directory.path() / "prescribed-axial.inp"; const auto output = directory.path() / "captured-results.h5"; writeText(input, axialDeck(0.1, 10.0)); fesa::SerialParallelFor serial; fesa::MklPardisoSolver solver; CapturingResultsWriter writer; fesa::LinearStaticAnalysis analysis{serial, solver, writer}; const auto status = analysis.run({input, output}); ASSERT_TRUE(status.isOk()); EXPECT_EQ(writer.outputPath(), output); EXPECT_EQ(writer.nodeCount(), 2U); EXPECT_TRUE(writer.diagnostics().empty()); const auto& state = writer.state(); ASSERT_EQ(state.displacement().size(), 12U); EXPECT_EQ(state.identity().stepName, "Step-1"); EXPECT_EQ(state.identity().frameIndex, 0U); // EA/L = 100 for this fixture, so u_tip = 0.1 + 10/100 = 0.2. EXPECT_NEAR(state.displacement()[0U], 0.1, 1.0e-12); EXPECT_NEAR(state.displacement()[6U], 0.2, 2.0e-10); EXPECT_NEAR(state.externalForce()[6U], 10.0, 1.0e-12); EXPECT_NEAR(state.internalForce()[0U], -10.0, 1.0e-9); EXPECT_NEAR(state.internalForce()[6U], 10.0, 1.0e-9); EXPECT_NEAR(state.reaction()[0U], -10.0, 1.0e-9); EXPECT_NEAR(state.residual()[6U], 0.0, 1.0e-9); EXPECT_EQ(state.endpointResults().size(), 2U); EXPECT_EQ(state.gaussResults().size(), 2U); EXPECT_EQ(state.stressResults().size(), 2U); }