#include "fesa/analysis/linear_static_analysis.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis.h" #include "fesa/app/fesa_application.h" #include "fesa/assembly/parallel_for.h" #include "fesa/results/results_writer.h" #include "fesa/solvers/linear/mkl_pardiso_solver.h" 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-surface-pressure-linear-" + 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 surface pressure 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 surface pressure input."}; } } std::string ShellDeck(const std::string& source_type, const std::string& connectivity, const bool prescribed_displacement, const bool with_pressure) { std::string boundary; if (prescribed_displacement) { boundary = R"inp(*Boundary N1, 1, 6 N2, 1, 2 N2, 3, 3, 5.0 N2, 4, 6 N3, 1, 6 N4, 1, 6 )inp"; } else { boundary = "*Boundary\nAll, 1, 6\n"; } const std::string surface = with_pressure ? "*Surface, TYPE=ELEMENT, NAME=Pressure\n" "Shell-1.1, SPOS\n" : ""; const std::string dsload = with_pressure ? "*Dsload\nPressure, P, 4.0\n" : ""; return std::string{R"inp(*Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. 3, 1., 1., 0. 4, 0., 1., 0. *Element, type=)inp"} + source_type + "\n1, " + connectivity + R"inp( *Elset, elset=ShellSet 1 *Shell Section, elset=ShellSet, material=Steel 0.1 *End Part *Assembly, name=Assembly *Instance, name=Shell-1, part=ShellPart *End Instance )inp" + surface + R"inp(*Nset, nset=N1, instance=Shell-1 1 *Nset, nset=N2, instance=Shell-1 2 *Nset, nset=N3, instance=Shell-1 3 *Nset, nset=N4, instance=Shell-1 4 *Nset, nset=All, instance=Shell-1 1, 2, 3, 4 *End Assembly *Material, name=Steel *Elastic 1000., 0.25 )inp" + boundary + R"inp(*Step, name=Load, nlgeom=NO *Static 0.1, 1., 0.01, 1. )inp" + dsload + "*End Step\n"; } class CapturingResultsWriter final : public fesa::ResultsWriter { public: fesa::Status Write(const std::filesystem::path&, const fesa::Domain&, const fesa::AnalysisState& state, const std::vector&) override { state_ = std::make_unique(state); return fesa::Status::Ok(); } const fesa::AnalysisState& State() const { if (!state_) { throw std::logic_error{"No linear state was captured."}; } return *state_; } private: std::unique_ptr state_; }; fesa::AnalysisState RunLinear(const std::filesystem::path& input, const std::filesystem::path& output) { fesa::SerialParallelFor serial; fesa::MklPardisoSolver solver; CapturingResultsWriter writer; std::unique_ptr analysis = std::make_unique(serial, solver, writer); const fesa::Status status = analysis->Run({input, output}); if (!status.IsOk()) { throw std::runtime_error{status.Diagnostics().empty() ? "Linear pressure analysis failed." : status.Diagnostics().front().code}; } return writer.State(); } std::array SumTranslation(const fesa::Vector& values) { std::array sum{}; for (std::size_t node = 0U; node < values.Size() / 6U; ++node) { for (std::size_t component = 0U; component < 3U; ++component) { sum[component] += values[node * 6U + component]; } } return sum; } } // namespace TEST(LinearShellSurfacePressure, UsesReferenceNormalAndAreaAfterPrescribedDisplacement) { TempDirectory directory{"reference-configuration"}; const auto input = directory.Path() / "prescribed.inp"; WriteText(input, ShellDeck("S4", "1, 2, 3, 4", true, true)); const fesa::AnalysisState state = RunLinear(input, directory.Path() / "captured.h5"); EXPECT_DOUBLE_EQ(state.Displacement()[8U], 5.0); EXPECT_EQ(SumTranslation(state.ExternalForce()), (std::array{0.0, 0.0, -4.0})); } TEST(LinearShellSurfacePressure, AssemblesBeforeProjectionAndIncludesConstrainedReaction) { TempDirectory directory{"reaction"}; const auto input = directory.Path() / "reaction.inp"; WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true)); const fesa::AnalysisState state = RunLinear(input, directory.Path() / "captured.h5"); EXPECT_EQ(SumTranslation(state.ExternalForce()), (std::array{0.0, 0.0, -4.0})); EXPECT_EQ(SumTranslation(state.Reaction()), (std::array{0.0, 0.0, 4.0})); } TEST(LinearShellSurfacePressure, SatisfiesFreeResidualGlobalResultantAndMoment) { TempDirectory directory{"equilibrium"}; const auto input = directory.Path() / "equilibrium.inp"; WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true)); const fesa::AnalysisState state = RunLinear(input, directory.Path() / "captured.h5"); for (const double value : state.Equilibrium()) { EXPECT_NEAR(value, 0.0, 1.0e-8); } EXPECT_NEAR(state.VerificationMetrics()[0U], 0.0, 1.0e-10); } TEST(LinearShellSurfacePressure, PreservesS4S4rMeshAndCyclicOrderInvariance) { TempDirectory directory{"source-invariance"}; const auto s4_input = directory.Path() / "s4.inp"; const auto s4r_input = directory.Path() / "s4r.inp"; const auto cyclic_input = directory.Path() / "cyclic.inp"; WriteText(s4_input, ShellDeck("S4", "1, 2, 3, 4", false, true)); WriteText(s4r_input, ShellDeck("S4R", "1, 2, 3, 4", false, true)); WriteText(cyclic_input, ShellDeck("S4", "2, 3, 4, 1", false, true)); const fesa::AnalysisState s4 = RunLinear(s4_input, directory.Path() / "s4.h5"); const fesa::AnalysisState s4r = RunLinear(s4r_input, directory.Path() / "s4r.h5"); const fesa::AnalysisState cyclic = RunLinear(cyclic_input, directory.Path() / "cyclic.h5"); ASSERT_EQ(s4.ExternalForce().Size(), s4r.ExternalForce().Size()); ASSERT_EQ(s4.ExternalForce().Size(), cyclic.ExternalForce().Size()); for (std::size_t index = 0U; index < s4.ExternalForce().Size(); ++index) { EXPECT_DOUBLE_EQ(s4.ExternalForce()[index], s4r.ExternalForce()[index]); EXPECT_DOUBLE_EQ(s4.ExternalForce()[index], cyclic.ExternalForce()[index]); EXPECT_DOUBLE_EQ(s4.Reaction()[index], s4r.Reaction()[index]); EXPECT_DOUBLE_EQ(s4.Reaction()[index], cyclic.Reaction()[index]); } } TEST(LinearShellSurfacePressure, WritesSchemaZeroWithoutPressureDataset) { TempDirectory directory{"schema-zero"}; const auto input = directory.Path() / "pressure.inp"; const auto output = directory.Path() / "pressure_results.h5"; WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true)); EXPECT_EQ(fesa::FesaApplication{}.Run({input.string()}), 0); ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0); const hid_t file = H5Fopen(output.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT); ASSERT_GE(file, 0); const hid_t metadata = H5Gopen2(file, "/metadata", H5P_DEFAULT); ASSERT_GE(metadata, 0); const hid_t schema = H5Aopen(metadata, "schema_version", H5P_DEFAULT); ASSERT_GE(schema, 0); std::uint64_t schema_version = 0U; ASSERT_GE(H5Aread(schema, H5T_NATIVE_UINT64, &schema_version), 0); EXPECT_EQ(schema_version, 0U); EXPECT_EQ(H5Lexists(file, "/steps/Step-1/frames/0/surface_pressure", H5P_DEFAULT), 0); EXPECT_EQ(H5Lexists(file, "/pressure", H5P_DEFAULT), 0); EXPECT_GE(H5Aclose(schema), 0); EXPECT_GE(H5Gclose(metadata), 0); EXPECT_GE(H5Fclose(file), 0); }