#include #include #include #include #include #include #include #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" namespace { class TemporaryInputFile { public: TemporaryInputFile(const std::string& stem, const std::string& content) : path_{std::filesystem::temp_directory_path() / ("fesa-hht-domain-" + stem + ".inp")} { std::ofstream stream{path_, std::ios::binary | std::ios::trunc}; stream.write(content.data(), static_cast(content.size())); if (!stream) { throw std::runtime_error{"Unable to create dynamic input fixture."}; } } ~TemporaryInputFile() { std::error_code error; std::filesystem::remove(path_, error); } const std::filesystem::path& Path() const noexcept { return path_; } private: std::filesystem::path path_; }; fesa::Result MapText(const std::string& stem, const std::string& content) { const TemporaryInputFile input{stem, content}; auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); if (!parsed.HasValue()) { return fesa::Result::Failure(parsed.GetStatus()); } return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status, const std::string& code) { for (const auto& diagnostic : status.Diagnostics()) { if (diagnostic.code == code) { return &diagnostic; } } return nullptr; } std::string DynamicDeck() { return R"inp(*Preprint, echo=NO, model=NO, history=NO, contact=NO *Part, name=ShellPart *Node 1, 0., 0., 0. 2, 1., 0., 0. 3, 1., 1., 0. 4, 0., 1., 0. *Element, type=S4R 1, 1, 2, 3, 4 *Elset, elset=ShellSet 1 *Shell Section, elset=ShellSet, material=Steel 0.5, 5 *End Part *Assembly, name=Assembly *Instance, name=Shell-1, part=ShellPart *End Instance *Nset, nset=Root, instance=Shell-1 1 *Nset, nset=Tip, instance=Shell-1 3 *End Assembly *Material, name=Steel *Density 2700., *Elastic 2.1e11, 0.3 *Boundary Root, 1, 6 *Step, name=Dynamic-Load, nlgeom=YES, inc=1000 *Dynamic, direct 0.1, 10., *Cload Tip, 1, -1000. *Restart, write, frequency=0 *Output, field, variable=PRESELECT, frequency=1 *Output, history, variable=PRESELECT *End Step )inp"; } std::string ReplaceOnce(std::string text, const std::string& from, const std::string& to) { const auto position = text.find(from); if (position == std::string::npos) { throw std::logic_error{"Dynamic fixture mutation source was not found."}; } text.replace(position, from.size(), to); return text; } std::filesystem::path RepositoryRoot() { auto path = std::filesystem::path{__FILE__}.parent_path(); for (int parent = 0; parent < 4; ++parent) { path = path.parent_path(); } return path; } TEST(HhtDynamicDomainMapping, MapsDirectScheduleDensityAndSourceOrder) { auto result = MapText("accepted", DynamicDeck()); ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : result.GetStatus().Diagnostics().front().message); const fesa::Domain& domain = result.Value(); ASSERT_NE(domain.Dynamic(), nullptr); const auto& dynamic = *domain.Dynamic(); EXPECT_EQ(dynamic.source_step_name_raw, "Dynamic-Load"); EXPECT_EQ(dynamic.source_step_name_canonical, "DYNAMIC-LOAD"); EXPECT_DOUBLE_EQ(dynamic.initial_increment, 0.1); EXPECT_DOUBLE_EQ(dynamic.time_period, 10.0); EXPECT_EQ(dynamic.increment_count, 100U); EXPECT_EQ(dynamic.maximum_increments, 1000U); EXPECT_DOUBLE_EQ(dynamic.alpha, -0.05); EXPECT_DOUBLE_EQ(dynamic.beta, 0.275625); EXPECT_DOUBLE_EQ(dynamic.gamma, 0.55); ASSERT_EQ(dynamic.boundaries.size(), 1U); ASSERT_EQ(dynamic.loads.size(), 1U); EXPECT_EQ(dynamic.loads[0U].dof, 1); ASSERT_EQ(domain.Densities().size(), 1U); EXPECT_DOUBLE_EQ(domain.Densities()[0U].rho, 2700.0); ASSERT_EQ(domain.Steps().Size(), 1U); EXPECT_TRUE(domain.Steps()[0U].IsDynamic()); EXPECT_FALSE(domain.Steps()[0U].IsNonlinearStatic()); EXPECT_EQ(domain.Steps()[0U].Dynamic(), domain.Dynamic()); ASSERT_EQ(domain.Warnings().size(), 4U); for (const auto& warning : domain.Warnings()) { EXPECT_EQ(warning.code, "ignored-output-request"); } } TEST(HhtDynamicDomainMapping, MapsOriginalReferenceDeckWithoutMutation) { const auto input_path = RepositoryRoot() / "reference" / "shellNL-dyn-meshfine" / "shellNL-dyn-meshfine.inp"; auto parsed = fesa::AbaqusInputReader{}.Read(input_path); ASSERT_TRUE(parsed.HasValue()); auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : result.GetStatus().Diagnostics().front().message); ASSERT_NE(result.Value().Dynamic(), nullptr); EXPECT_EQ(result.Value().Dynamic()->increment_count, 100U); EXPECT_EQ(result.Value().Dynamic()->maximum_increments, 1000U); EXPECT_EQ(result.Value().Dynamic()->loads.size(), 3U); } TEST(HhtDynamicDomainMapping, RejectsMalformedScheduleAndUnsupportedControls) { struct InvalidCase { std::string name; std::string deck; std::string diagnostic; }; const std::string base = DynamicDeck(); const std::vector cases{ {"noninteger-grid", ReplaceOnce(base, "0.1, 10.,", "0.3, 10.,"), "invalid-dynamic-schedule"}, {"increment-limit", ReplaceOnce(base, "inc=1000", "inc=99"), "invalid-dynamic-schedule"}, {"nonpositive-data", ReplaceOnce(base, "0.1, 10.,", "0., 10.,"), "invalid-dynamic-data"}, {"integrator-override", ReplaceOnce(base, "*Dynamic, direct", "*Dynamic, direct, alpha=-0.1"), "unsupported-dynamic-parameter"}, {"rotational-load", ReplaceOnce(base, "Tip, 1, -1000.", "Tip, 4, 0."), "unsupported-dynamic-load"}, {"nonzero-boundary", ReplaceOnce(base, "Root, 1, 6", "Root, 1, 6, 1."), "unsupported-dynamic-boundary"}, {"amplitude-boundary", ReplaceOnce(base, "*Boundary\n", "*Boundary, amplitude=A\n"), "unsupported-dynamic-boundary"}, {"missing-density", ReplaceOnce(base, "*Density\n2700.,\n", ""), "missing-material-density"}, {"unresolved-load", ReplaceOnce(base, "Tip, 1, -1000.", "Missing, 1, -1000."), "unresolved-reference"}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.name); auto result = MapText(test_case.name, test_case.deck); ASSERT_FALSE(result.HasValue()); ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.diagnostic), nullptr); } } } // namespace