#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/analysis/thermal_time_grid.h" #include "fesa/fem/dof_manager.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" namespace { class TemporaryInput final { public: explicit TemporaryInput(const std::string& content) { // Atomically claim a directory: separate CTest processes never share files. do { directory_ = std::filesystem::temp_directory_path() / ("fesa-thermal-domain-" + std::to_string(std::random_device{}()) + "-" + std::to_string(next_id_.fetch_add(1U))); } while (!std::filesystem::create_directory(directory_)); path_ = directory_ / "model.inp"; std::ofstream output{path_, std::ios::binary | std::ios::trunc}; output << content; if (!output) throw std::runtime_error{"Unable to write thermal fixture."}; } ~TemporaryInput() { std::error_code error; std::filesystem::remove(path_, error); std::filesystem::remove(directory_, error); } const std::filesystem::path& Path() const noexcept { return path_; } private: static std::atomic next_id_; std::filesystem::path directory_; std::filesystem::path path_; }; std::atomic TemporaryInput::next_id_{0U}; fesa::Result MapText(const std::string& text) { const TemporaryInput input{text}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); if (!parsed.HasValue()) return fesa::Result::Failure(parsed.GetStatus()); return fesa::AbaqusDomainMapper{}.Map(parsed.Value()); } std::string ThermalDeck() { return R"inp(*Part, name=P *Node 1, 0., 0., 0. 2, 1., 0., 0. 3, 1., 1., 0. 4, 0., 1., 0. *Element, type=DS4 1, 1, 2, 3, 4 *Elset, elset=E 1 *Shell Section, elset=E, material=M .5, 5 *End Part *Assembly, name=A *Instance, name=I, part=P *End Instance *Nset, nset=Bottom, instance=I 1 2 *End Assembly *Material, name=M *Density 7850., *Conductivity 54., *Specific Heat 460., *Initial Conditions, type=TEMPERATURE Bottom, 20. *Boundary Bottom, 11, 11 *Step, name=Heat, inc=100 *Heat Transfer 1., 100. *Boundary Bottom, 15, 15, 30. *Cflux Bottom, 11, -2. *Cflux Bottom, 11, -3. *End Step )inp"; } std::string Replace(std::string deck, const std::string& from, const std::string& to) { const auto position = deck.find(from); if (position == std::string::npos) throw std::runtime_error{"Missing fixture token."}; deck.replace(position, from.size(), to); return deck; } std::size_t LineOf(const std::string& deck, const std::string& token) { const auto position = deck.find(token); if (position == std::string::npos) throw std::runtime_error{"Missing source token."}; return 1U + static_cast( std::count(deck.begin(), deck.begin() + position, '\n')); } void ExpectRejected(const std::string& deck, const std::string& keyword = "") { const TemporaryInput input{deck}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_FALSE(result.HasValue()); ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); const auto& diagnostic = result.GetStatus().Diagnostics().front(); EXPECT_EQ(diagnostic.severity, fesa::Severity::kError); EXPECT_EQ(diagnostic.location.file, input.Path()); EXPECT_GT(diagnostic.location.line, 0U); if (!keyword.empty()) EXPECT_EQ(diagnostic.keyword, keyword); } std::string Number(double value) { std::ostringstream stream; stream << std::setprecision(std::numeric_limits::max_digits10) << value; return stream.str(); } TEST(ThermalDomainMapping, MapsDs4MaterialsTargetsAndAdditiveFluxes) { const auto result = MapText(ThermalDeck()); ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : result.GetStatus().Diagnostics().front().message); const auto& domain = result.Value(); ASSERT_EQ(domain.ThermalElements().Size(), 1U); ASSERT_EQ(domain.ThermalMaterials().Size(), 1U); EXPECT_DOUBLE_EQ(domain.ThermalMaterials()[0U].Conductivity(), 54.0); ASSERT_NE(domain.ThermalStep(), nullptr); EXPECT_EQ(domain.ThermalStep()->name, "Heat"); EXPECT_EQ(domain.ThermalStep()->maximum_increments, 100U); EXPECT_EQ(domain.ThermalModelBoundaries().size(), 2U); EXPECT_EQ(domain.ThermalStep()->Boundaries().size(), 2U); ASSERT_EQ(domain.ThermalStep()->Loads().size(), 4U); EXPECT_DOUBLE_EQ(domain.ThermalStep()->Loads()[0U].power, -2.0); EXPECT_DOUBLE_EQ(domain.ThermalStep()->Loads()[3U].power, -3.0); } TEST(ThermalDomainMapping, RejectsInteriorAndConflictingThermalBoundaries) { auto interior = ThermalDeck(); interior.replace(interior.find("Bottom, 11, 11\n"), 14U, "Bottom, 12, 12\n"); const auto invalid_dof = MapText(interior); ASSERT_FALSE(invalid_dof.HasValue()); EXPECT_EQ(invalid_dof.GetStatus().Diagnostics()[0U].code, "invalid-dof"); auto conflicting = ThermalDeck(); conflicting.replace(conflicting.find("*Cflux"), 0U, "*Boundary\nBottom, 15, 15, 99.\n"); const auto invalid_conflict = MapText(conflicting); ASSERT_FALSE(invalid_conflict.HasValue()); EXPECT_EQ(invalid_conflict.GetStatus().Diagnostics()[0U].code, "conflicting-boundary-condition"); } TEST(ThermalDomainMapping, AcceptsSectionPointSpellingsAndRejectsUnsupportedForms) { const std::vector> cases{ {".5\n", true}, {".5, 5\n", true}, {".5, 05\n", true}, {".5, 4\n", false}, {".5, 6\n", false}, {".5, 5, 0.\n", false}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.first); auto deck = ThermalDeck(); const auto start = deck.find(".5, 5\n"); deck.replace(start, std::string{".5, 5\n"}.size(), test_case.first); EXPECT_EQ(MapText(deck).HasValue(), test_case.second); } } TEST(ThermalDomainMapping, RejectsThermalMaterialAndControlForms) { struct Case { std::string from; std::string to; }; const std::vector cases{ {"54.,\n", "54., 1.\n"}, {"460.,\n", "460.\n461.\n"}, {"7850.,\n", "-1.,\n"}, {"*Conductivity\n", "*Conductivity, dependencies=1\n"}, {"*Specific Heat\n460.,\n", ""}, {"*Specific Heat\n460.,\n", "*Specific Heat\n460.,\n*Elastic\n1., .3\n"}, {"*Heat Transfer\n1., 100.\n", "*Heat Transfer, steady state\n1., 100.\n"}, {"*Heat Transfer\n1., 100.\n", "*Heat Transfer\n0., 100.\n"}, {"inc=100", "inc=99"}, {"*Step, name=Heat, inc=100", "*Step, name=Heat, nlgeom=YES, inc=100"}, {"*Step, name=Heat, inc=100", "*Step, name=Heat, perturbation, inc=100"}}; for (const auto& test_case : cases) { SCOPED_TRACE(test_case.to); auto deck = ThermalDeck(); const auto position = deck.find(test_case.from); ASSERT_NE(position, std::string::npos); deck.replace(position, test_case.from.size(), test_case.to); EXPECT_FALSE(MapText(deck).HasValue()); } } TEST(ThermalDomainMapping, RejectsThermalNoOpAndProcedureViolations) { const std::vector> cases{ {"*Part, name=P", "*Preprint, echo=NO, model=NO, history=NO\n*Part, name=P"}, {"1., 100.\n", "1., 100., , , ,\n"}, {"*Cflux", "*Static\n.1, 1., .1, 1.\n*Cflux"}, {"*End Step", "*Output, field\n*Node Output\nNT\n*End Step"}}; for (const auto& test_case : cases) { auto deck = ThermalDeck(); const auto position = deck.find(test_case.first); ASSERT_NE(position, std::string::npos); deck.replace(position, test_case.first.size(), test_case.second); EXPECT_FALSE(MapText(deck).HasValue()); } } TEST(ThermalDomainMapping, AppliesSameDofStepOverrideAndDefaultValues) { auto deck = ThermalDeck(); const auto first = deck.find("Bottom, 15, 15, 30."); deck.replace(first, std::string{"Bottom, 15, 15, 30."}.size(), "Bottom, 11, 11, 30."); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); ASSERT_EQ(result.Value().ThermalModelBoundaries().size(), 2U); ASSERT_EQ(result.Value().ThermalStep()->Boundaries().size(), 2U); EXPECT_DOUBLE_EQ(result.Value().ThermalModelBoundaries()[0U].value, 0.0); EXPECT_DOUBLE_EQ(result.Value().ThermalStep()->Boundaries()[0U].value, 30.0); } TEST(ThermalDomainMapping, MapsApprovedShellThermalReference) { auto path = std::filesystem::path{__FILE__}.parent_path(); for (int index = 0; index < 4; ++index) path = path.parent_path(); path /= "reference/shell-thermal/shell-thermal.inp"; const auto parsed = fesa::AbaqusInputReader{}.Read(path); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_TRUE(result.HasValue()) << (result.HasValue() ? "" : result.GetStatus().Diagnostics().front().message); const auto& domain = result.Value(); ASSERT_EQ(domain.Nodes().size(), 729U); ASSERT_EQ(domain.ThermalElements().Size(), 676U); ASSERT_EQ(domain.ThermalMaterials().Size(), 1U); ASSERT_EQ(domain.ThermalSections().size(), 1U); ASSERT_NE(domain.ThermalStep(), nullptr); const auto& step = *domain.ThermalStep(); EXPECT_EQ(step.name, "Step-1"); EXPECT_EQ(step.maximum_increments, 100000U); EXPECT_EQ(step.nominal_increment, 1.); EXPECT_EQ(step.time_period, 100.); const auto grid = fesa::ThermalTimeGrid::Create( step.time_period, step.nominal_increment, step.maximum_increments); ASSERT_TRUE(grid.HasValue()); EXPECT_EQ(grid.Value().Count(), 100U); EXPECT_EQ(grid.Value().Time(37U), 37.); EXPECT_EQ(domain.ThermalMaterials()[0].Name(), "Material-1"); EXPECT_EQ(domain.ThermalMaterials()[0].Conductivity(), 54.); EXPECT_EQ(domain.ThermalMaterials()[0].Density(), 7850.); EXPECT_EQ(domain.ThermalMaterials()[0].SpecificHeat(), 460.); EXPECT_EQ(domain.ThermalSections()[0].name, "Set-1"); EXPECT_EQ(domain.ThermalSections()[0].thickness, .5); EXPECT_EQ(domain.ThermalSections()[0].location.file, path); EXPECT_EQ(domain.ThermalSections()[0].location.line, 1421U); for (std::size_t i = 0; i < domain.Nodes().size(); ++i) { EXPECT_EQ(domain.Nodes()[i].source_id.instance_name, "Part-1-1"); EXPECT_EQ(domain.Nodes()[i].source_id.source_label, static_cast(i + 1U)); } for (std::size_t i = 0; i < domain.ThermalElements().Size(); ++i) { EXPECT_EQ(domain.ThermalElements()[i].SourceId().instance_name, "Part-1-1"); EXPECT_EQ(domain.ThermalElements()[i].SourceId().source_label, static_cast(i + 1U)); } std::vector expected_boundary_labels{2, 5, 6, 7}; for (std::int64_t label = 58; label <= 105; ++label) expected_boundary_labels.push_back(label); for (std::int64_t label = 298; label <= 441; ++label) expected_boundary_labels.push_back(label); ASSERT_EQ(domain.ThermalModelBoundaries().size(), expected_boundary_labels.size()); ASSERT_EQ(domain.ThermalModelBoundaries().size(), step.Boundaries().size()); for (std::size_t i = 0; i < step.Boundaries().size(); ++i) { const auto& model_bc = domain.ThermalModelBoundaries()[i]; const auto& step_bc = step.Boundaries()[i]; EXPECT_EQ(model_bc.node_index, step_bc.node_index); EXPECT_EQ(domain.Nodes()[model_bc.node_index].source_id.source_label, expected_boundary_labels[i]); EXPECT_EQ(model_bc.dof, 11); EXPECT_EQ(step_bc.dof, 11); EXPECT_EQ(model_bc.value, 0.); EXPECT_EQ(step_bc.value, 1000.); EXPECT_EQ(model_bc.location.file, path); EXPECT_LT(model_bc.location.line, step_bc.location.line); } ASSERT_EQ(domain.ElementSurfaces().size(), 1U); ASSERT_EQ(domain.ElementSurfaces()[0].facets.size(), 169U); EXPECT_EQ(domain.ElementSurfaces()[0].facets.front().source_element_label, 170); EXPECT_EQ(domain.ElementSurfaces()[0].facets.back().source_element_label, 338); EXPECT_EQ(domain.ElementSurfaces()[0].facets[0].source_element_type, "DS4"); ASSERT_EQ(domain.Warnings().size(), 6U); for (const auto& block : parsed.Value().blocks) { if (block.canonical_name != "PREPRINT" && block.canonical_name != "PHYSICAL CONSTANTS" && block.canonical_name != "SURFACE" && block.canonical_name != "RESTART" && block.canonical_name != "OUTPUT") continue; EXPECT_EQ(std::count_if(domain.Warnings().begin(), domain.Warnings().end(), [&](const auto& warning) { return warning.location.file == path && warning.location.line == block.location.line && warning.keyword == block.canonical_name; }), 1); } } TEST(ThermalDomainMapping, StrictOutputAndRestartNoOps) { for (const std::string block : {"*Output, field\n", "*Output, field, variable=ALL\n", "*Output, field, variable=PRESELECT, frequency=0\n", "*Output, history\n", "*Output, history, frequency=7\n", "*Output, history, frequency=0, variable=PRESELECT\n", "*Output, field, history, variable=PRESELECT\n", "*Output, field=YES, variable=PRESELECT\n", "*Output, field, variable=PRESELECT\nNT\n", "*Output, history, frequency=0\nNT\n", "*Restart\n", "*Restart, write\n", "*Restart, frequency=0\n", "*Restart, write=YES, frequency=0\n", "*Restart, write, frequency=1\n", "*Restart, write, frequency=0, overlay\n", "*Restart, write, frequency=0\n0\n"}) { SCOPED_TRACE(block); ExpectRejected(Replace(ThermalDeck(), "*End Step", block + "*End Step")); } } TEST(ThermalDomainMapping, EveryAcceptedNoOpHasExactlyOneSourceWarning) { const std::vector> blocks{ {"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n", "PREPRINT"}, {"*Physical Constants, absolute zero=-273.15\n", "PHYSICAL CONSTANTS"}, {"*Surface, type=ELEMENT, name=S\nI.1, SPOS\n", "SURFACE"}, {"*Restart, write, frequency=0\n", "RESTART"}, {"*Output, field, variable=PRESELECT\n", "OUTPUT"}, {"*Output, history, frequency=0\n", "OUTPUT"}}; auto deck = ThermalDeck(); deck = blocks[0].first + deck; deck = Replace(deck, "*Material", blocks[1].first + "*Material"); deck = Replace(deck, "*End Assembly", blocks[2].first + "*End Assembly"); for (std::size_t i = 3U; i < blocks.size(); ++i) deck = Replace(deck, "*End Step", blocks[i].first + "*End Step"); const TemporaryInput input{deck}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_TRUE(result.HasValue()); const auto& warnings = result.Value().Warnings(); ASSERT_EQ(warnings.size(), blocks.size()); for (const auto& block : blocks) { const auto line = LineOf(deck, block.first); EXPECT_EQ(std::count_if(warnings.begin(), warnings.end(), [&](const auto& w) { return w.severity == fesa::Severity::kWarning && w.keyword == block.second && w.location.file == input.Path() && w.location.line == line; }), 1); } EXPECT_DOUBLE_EQ( result.Value().ThermalInitialConditions()[0].station_values[0], 20.); } TEST(ThermalDomainMapping, SurfaceFacetsAreResolvedAndPreserved) { for (const std::string side : {"SPOS", "SNEG"}) { auto deck = Replace( ThermalDeck(), "*End Assembly", "*Elset, elset=Faces, instance=I\n1\n" "*Surface, type=ELEMENT, name=Direct\nI.1, " + side + "\n*Surface, type=ELEMENT, name=SetSurface\nFaces, " + side + "\n*End Assembly"); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); const auto& surfaces = result.Value().ElementSurfaces(); ASSERT_EQ(surfaces.size(), 2U); for (std::size_t i = 0; i < surfaces.size(); ++i) { ASSERT_EQ(surfaces[i].facets.size(), 1U); const auto& facet = surfaces[i].facets[0]; EXPECT_EQ(facet.element_index, 0U); EXPECT_EQ(facet.source_element_label, 1); EXPECT_EQ(facet.instance_name_raw, "I"); EXPECT_EQ(facet.part_name_raw, "P"); EXPECT_EQ(facet.source_element_type, "DS4"); EXPECT_EQ(facet.side, side == "SPOS" ? fesa::ShellSurfaceSide::kSpos : fesa::ShellSurfaceSide::kSneg); EXPECT_EQ(facet.member_kind, i == 0U ? fesa::SurfaceMemberKind::kDirect : fesa::SurfaceMemberKind::kElset); EXPECT_EQ(facet.first_member_location.line, LineOf(deck, (i == 0U ? "I.1, " : "Faces, ") + side)); } } } TEST(ThermalDomainMapping, RejectsUnresolvedOrInvalidSurfaces) { for (const std::string row : {"Missing.1, SPOS", "I.99, SPOS", "Missing, SPOS", "E, SPOS", "P.1, SPOS", "I.E, SPOS", "I.1, S1", "I.1", "I.1, SPOS, 1"}) { SCOPED_TRACE(row); ExpectRejected( Replace(ThermalDeck(), "*End Assembly", "*Surface, type=ELEMENT, name=S\n" + row + "\n*End Assembly"), "SURFACE"); } for (const std::string header : {"*Surface, name=S", "*Surface, type=ELEMENT", "*Surface, type=NODE, name=S", "*Surface, type=ELEMENT, name=S, internal"}) { ExpectRejected(Replace(ThermalDeck(), "*End Assembly", header + "\nI.1, SPOS\n*End Assembly"), "SURFACE"); } ExpectRejected(Replace(ThermalDeck(), "*End Assembly", "*Surface, type=ELEMENT, name=S\n*End Assembly"), "SURFACE"); } TEST(ThermalDomainMapping, PreprintAndPhysicalConstantsAreStrictAndSourceAware) { for (const std::string block : {"*Preprint\n", "*Preprint, echo=NO, model=NO, history=NO\n", "*Preprint, echo=NO, model=NO, history=NO, contact=YES\n", "*Preprint, echo=NO, model=NO, history=NO, contact\n", "*Preprint, echo=NO, model=NO, history=NO, contact=NO, unknown=NO\n", "*Preprint, echo=NO, model=NO, history=NO, contact=NO\ndata\n", "*Physical Constants\n", "*Physical Constants, absolute zero=nan\n", "*Physical Constants, absolute zero=inf\n", "*Physical Constants, absolute zero=\n", "*Physical Constants, absolute zero=-273.15, stefan boltzmann=1\n", "*Physical Constants, absolute zero=-273.15\ndata\n"}) { SCOPED_TRACE(block); ExpectRejected(block + ThermalDeck()); } auto structural = Replace(ThermalDeck(), "type=DS4", "type=S4"); structural = Replace(structural, "*Density\n7850.,\n*Conductivity\n54.,\n*Specific Heat\n460.,\n", "*Elastic\n210000., .3\n"); structural = Replace( structural, "*Initial Conditions, type=TEMPERATURE\nBottom, 20.\n", ""); structural = Replace(structural, "Bottom, 11, 11", "Bottom, 1, 1"); structural = Replace(structural, "*Heat Transfer\n1., 100.", "*Static\n1., 1., 1., 1."); structural = Replace(structural, "*Boundary\nBottom, 15, 15, 30.\n*Cflux\nBottom, 11, " "-2.\n*Cflux\nBottom, 11, -3.\n", ""); const std::string block = "*Physical Constants, absolute zero=-273.15\n"; structural = block + structural; const auto result = MapText(structural); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "PHYSICAL CONSTANTS"); EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, 1U); } TEST(ThermalDomainMapping, RejectsChildOutputUnknownAndWrongNoOpScopes) { const std::vector blocks{ "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n", "*Physical Constants, absolute zero=-273.15\n", "*Restart, write, frequency=0\n", "*Output, field, variable=PRESELECT\n", "*Surface, type=ELEMENT, name=S\nI.1, SPOS\n"}; for (std::size_t i = 0; i < blocks.size(); ++i) { ExpectRejected( Replace(ThermalDeck(), "*End Part", blocks[i] + "*End Part")); const auto target = i < 2U || i == 4U ? "*End Step" : "*Material"; ExpectRejected(Replace(ThermalDeck(), target, blocks[i] + target)); } for (const std::string child : {"NODE OUTPUT", "ELEMENT OUTPUT", "CONTACT OUTPUT", "ENERGY OUTPUT"}) { ExpectRejected(Replace(ThermalDeck(), "*End Step", "*Output, field, variable=PRESELECT\n*" + child + "\n*End Step"), child); } ExpectRejected(Replace(ThermalDeck(), "*End Step", "*Unknown Thermal Keyword\n*End Step")); } TEST(ThermalDomainMapping, MaterialConstantsAndSectionLinksAreStrict) { for (const auto& property : std::vector>{ {"Density", "7850."}, {"Conductivity", "54."}, {"Specific Heat", "460."}}) { const std::string original = "*" + property.first + "\n" + property.second + ",\n"; for (const std::string tail : {"\n", ",\n"}) { const auto result = MapText( Replace(ThermalDeck(), original, "*" + property.first + "\n" + property.second + tail)); ASSERT_TRUE(result.HasValue()) << property.first << tail; } for (const std::string value : {"0", "-1", "nan", "inf", "1e400", "", "1, 20", "1,,", "1\n2"}) { SCOPED_TRACE(property.first + ":" + value); ExpectRejected(Replace(ThermalDeck(), original, "*" + property.first + "\n" + value + "\n")); } for (const std::string option : {"dependencies=1", "type=ANISOTROPIC", "type=ORTHOTROPIC", "unknown=1"}) ExpectRejected(Replace(ThermalDeck(), "*" + property.first + "\n", "*" + property.first + ", " + option + "\n")); ExpectRejected(Replace(ThermalDeck(), original, ""), "MATERIAL"); ExpectRejected(Replace(ThermalDeck(), original, original + original)); } for (const std::string row : {"0", "-1", "nan", "inf", ".5,", ".5, 5.0", ".5, 3", ".5,5,", ".5\n.5"}) ExpectRejected(Replace(ThermalDeck(), ".5, 5\n", row + "\n"), "SHELL SECTION"); for (const std::string option : {"offset=0", "offset=SPOS", "composite", "nodal thickness"}) ExpectRejected(Replace(ThermalDeck(), "*Shell Section, elset=E, material=M", "*Shell Section, elset=E, material=M, " + option), "SHELL SECTION"); ExpectRejected(Replace(ThermalDeck(), "material=M", "material=Missing"), "SHELL SECTION"); ExpectRejected(Replace(ThermalDeck(), "*Shell Section, elset=E", "*Shell Section, elset=Missing"), "SHELL SECTION"); ExpectRejected(Replace(ThermalDeck(), "*Shell Section, elset=E, material=M\n.5, 5\n", ""), "ELEMENT"); ExpectRejected(Replace(ThermalDeck(), "*End Part", "*Shell Section, elset=E, material=M\n.5\n*End Part"), "SHELL SECTION"); } TEST(ThermalDomainMapping, InitialTemperatureDefaultsAndOverlaps) { const auto unspecified = MapText( Replace(ThermalDeck(), "*Initial Conditions, type=TEMPERATURE\nBottom, 20.\n", "")); ASSERT_TRUE(unspecified.HasValue()); EXPECT_TRUE(unspecified.Value().ThermalInitialConditions().empty()); for (const std::string row : {"1, 20.", "Bottom, 20."}) { const auto result = MapText( Replace(ThermalDeck(), "Bottom, 20.\n", "Bottom, 20.\n" + row + "\n")); ASSERT_TRUE(result.HasValue()); for (const auto& initial : result.Value().ThermalInitialConditions()) for (const auto temperature : initial.station_values) EXPECT_DOUBLE_EQ(temperature, 20.); } ExpectRejected( Replace(ThermalDeck(), "Bottom, 20.\n", "Bottom, 20.\n1, -20.\n"), "INITIAL CONDITIONS"); for (const std::string row : {"Missing, 20.", "99, 20.", "1", "1, 20., 30.", "1, nan", ", 20."}) ExpectRejected(Replace(ThermalDeck(), "Bottom, 20.\n", row + "\n"), "INITIAL CONDITIONS"); for (const std::string header : {"*Initial Conditions", "*Initial Conditions, type=STRESS", "*Initial Conditions, type=TEMPERATURE, amplitude=A"}) ExpectRejected( Replace(ThermalDeck(), "*Initial Conditions, type=TEMPERATURE", header), "INITIAL CONDITIONS"); ExpectRejected( Replace(ThermalDeck(), "*End Step", "*Initial Conditions, type=TEMPERATURE\n1, 1\n*End Step"), "INITIAL CONDITIONS"); } TEST(ThermalDomainMapping, InactiveTargetsFailWithOriginalKeywordAndIdentity) { const auto inactive = Replace(ThermalDeck(), "*Element, type=DS4", "99, 2., 2., 0.\n*Element, type=DS4"); const auto accepted = MapText(inactive); ASSERT_TRUE(accepted.HasValue()); EXPECT_EQ(accepted.Value().Nodes().size(), 5U); const auto view = fesa::AnalysisModel::Create(accepted.Value()); ASSERT_TRUE(view.HasValue()); EXPECT_EQ(view.Value().ActiveThermalNodes().size(), 4U); for (const auto& replacement : std::vector>{ {"Bottom, 20.", "99, 20."}, {"Bottom, 11, 11", "99, 11, 11"}, {"Bottom, 15, 15, 30.", "99, 15, 15, 30."}, {"Bottom, 11, -2.", "99, 11, -2."}}) { const auto deck = Replace(inactive, replacement.first, replacement.second); const auto result = MapText(deck); ASSERT_FALSE(result.HasValue()); const auto& diagnostic = result.GetStatus().Diagnostics()[0]; EXPECT_EQ(diagnostic.entity_identity, "99"); EXPECT_EQ(diagnostic.location.line, LineOf(deck, replacement.second)); EXPECT_NE(diagnostic.keyword, "THERMAL"); } } TEST(ThermalDomainMapping, ModelBoundaryInheritanceAndStepOverrideReachDofManager) { auto deck = Replace(ThermalDeck(), "Bottom, 11, 11\n", "Bottom, 11, 11, 7.\n4, 15, 15\n1, 11, 11, 7.\n"); deck = Replace(deck, "Bottom, 15, 15, 30.", "1, 11, 11, -30.\n1, 11, 11, -30.\n2, 15, 15"); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); const auto& domain = result.Value(); ASSERT_EQ(domain.ThermalModelBoundaries().size(), 4U); ASSERT_EQ(domain.ThermalStep()->Boundaries().size(), 3U); EXPECT_DOUBLE_EQ(domain.ThermalModelBoundaries()[0].value, 7.); EXPECT_DOUBLE_EQ(domain.ThermalStep()->Boundaries()[0].value, -30.); const auto model = fesa::AnalysisModel::Create(domain); ASSERT_TRUE(model.HasValue()); const auto dofs = fesa::DofManager::CreateThermal(model.Value()); ASSERT_TRUE(dofs.HasValue()); const std::vector expected_dofs{0U, 5U, 9U, 19U}; EXPECT_EQ(dofs.Value().ConstrainedDofs(), expected_dofs); const std::array expected_values{-30., 7., 0., 0.}; for (std::size_t i = 0; i < expected_values.size(); ++i) EXPECT_DOUBLE_EQ(dofs.Value().PrescribedValues()[i], expected_values[i]); } TEST(ThermalDomainMapping, BoundariesRejectConflictsAndUnsupportedRowsAtBothLevels) { for (const std::string original : {"Bottom, 11, 11\n", "Bottom, 15, 15, 30.\n"}) { const std::string equal = original.find("15") == std::string::npos ? "1, 11, 11, 0.\n" : "1, 15, 15, 30.\n"; EXPECT_TRUE( MapText(Replace(ThermalDeck(), original, original + equal)).HasValue()); const std::string conflict = original.find("15") == std::string::npos ? "1, 11, 11, 2.\n" : "1, 15, 15, 2.\n"; ExpectRejected(Replace(ThermalDeck(), original, original + conflict), "BOUNDARY"); for (const std::string row : {"Bottom, 12, 12", "Bottom, 13, 13", "Bottom, 14, 14", "Bottom, 11, 15", "Bottom, 11", "Bottom, 11, 11,", "Bottom, 11, 11, 1, 2", "Bottom, 15, 15, inf", "Missing, 11, 11"}) ExpectRejected(Replace(ThermalDeck(), original, row + "\n"), "BOUNDARY"); for (const std::string option : {"OP=NEW", "AMPLITUDE=A"}) ExpectRejected(Replace(ThermalDeck(), "*Boundary\n" + original, "*Boundary, " + option + "\n" + original), "BOUNDARY"); } ExpectRejected( Replace(ThermalDeck(), "*End Part", "*Boundary\n1, 11, 11\n*End Part"), "BOUNDARY"); ExpectRejected(Replace(ThermalDeck(), "*Heat Transfer", "*Boundary\n1, 11, 11\n*Heat Transfer"), "BOUNDARY"); } TEST(ThermalDomainMapping, SignedFluxRetainsRowOrderDuplicatesAndConstrainedTargets) { auto deck = Replace(ThermalDeck(), "1\n2\n*End Assembly", "2, 1, 2\n*End Assembly"); deck = Replace( deck, "*Cflux\nBottom, 11, -2.\n*Cflux\nBottom, 11, -3.", "*Cflux\n2, 15, 9.\nBottom, 11, -2.\n1, 11, 3.\n*Cflux\n1, 11, 3."); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); const auto& loads = result.Value().ThermalStep()->Loads(); ASSERT_EQ(loads.size(), 5U); const std::array nodes{1, 1, 0, 0, 0}; const std::array dofs{15, 11, 11, 11, 11}; const std::array powers{9., -2., -2., 3., 3.}; for (std::size_t i = 0; i < loads.size(); ++i) { EXPECT_EQ(loads[i].node_index, nodes[i]); EXPECT_EQ(loads[i].dof, dofs[i]); EXPECT_DOUBLE_EQ(loads[i].power, powers[i]); EXPECT_EQ(loads[i].source_order, i); } EXPECT_EQ(loads[1].location.line, loads[2].location.line); EXPECT_LT(loads[3].location.line, loads[4].location.line); } TEST(ThermalDomainMapping, FluxRejectsOptionsRowsAndWrongScopes) { for (const std::string row : {"Bottom, 12, 1", "Bottom, 13, 1", "Bottom, 14, 1", "Bottom, 11", "Bottom, 11, 1,", "Bottom, 11, inf", "Bottom, 11, nan", "Missing, 11, 1", ", 11, 1"}) ExpectRejected(Replace(ThermalDeck(), "Bottom, 11, -2.", row), "CFLUX"); for (const std::string option : {"OP=NEW", "AMPLITUDE=A", "unknown=1"}) ExpectRejected(Replace(ThermalDeck(), "*Cflux", "*Cflux, " + option), "CFLUX"); for (const std::string target : {"*Part", "*End Part", "*End Assembly", "*Step", "*Heat Transfer"}) ExpectRejected( Replace(ThermalDeck(), target, "*Cflux\n1, 11, 1\n" + target), "CFLUX"); } TEST(ThermalDomainMapping, TimeControlsUseSharedGridIncludingInputCellBoundaries) { struct Case { double increment; double period; unsigned count; double last; }; const double tiny = std::numeric_limits::denorm_min(); const std::vector cases{ {1., 100., 100, 1.}, {3., 10., 4, 1.}, {3., 2., 1, 2.}, {2., 2., 1, 2.}, {.3, .9, 3, .3}, {.3, std::nextafter(.9, 1.), 4, 1.6653345369377348e-16}, {6 * tiny, 20 * tiny, 3, 6 * tiny}, {5 * tiny, 17 * tiny, 4, 2 * tiny}}; for (const auto& item : cases) { SCOPED_TRACE(Number(item.increment) + ", " + Number(item.period)); auto deck = Replace(ThermalDeck(), "1., 100.", Number(item.increment) + ", " + Number(item.period)); deck = Replace(deck, "inc=100", "inc=" + std::to_string(item.count)); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); const auto& step = *result.Value().ThermalStep(); EXPECT_EQ(step.nominal_increment, item.increment); EXPECT_EQ(step.time_period, item.period); const auto grid = fesa::ThermalTimeGrid::Create( step.time_period, step.nominal_increment, step.maximum_increments); ASSERT_TRUE(grid.HasValue()); EXPECT_EQ(grid.Value().Count(), item.count); EXPECT_DOUBLE_EQ(grid.Value().Time(item.count), item.period); EXPECT_EQ(grid.Value().Delta(item.count), item.last); if (item.count > 1U) ExpectRejected(Replace(deck, "inc=" + std::to_string(item.count), "inc=" + std::to_string(item.count - 1U)), "HEAT TRANSFER"); } auto largest_grid = Replace(ThermalDeck(), "1., 100.", "1., 4294967295."); largest_grid = Replace(largest_grid, "inc=100", "inc=4294967295"); const auto upper = MapText(largest_grid); ASSERT_TRUE(upper.HasValue()); EXPECT_EQ(upper.Value().ThermalStep()->maximum_increments, 4294967295U); ExpectRejected(Replace(largest_grid, "1., 4294967295.", "1., 4294967296."), "HEAT TRANSFER"); const auto defaults = MapText(Replace(ThermalDeck(), ", inc=100", ", nlgeom=NO")); ASSERT_TRUE(defaults.HasValue()); EXPECT_EQ(defaults.Value().ThermalStep()->maximum_increments, 100U); for (const std::string option : {"", ", end=PERIOD"}) { for (int empty = 0; empty <= 3; ++empty) { auto deck = Replace( ThermalDeck(), "*Heat Transfer\n1., 100.", "*Heat Transfer" + option + "\n1., 100." + std::string(empty, ',')); EXPECT_TRUE(MapText(deck).HasValue()) << deck; } } } TEST(ThermalDomainMapping, RejectsInvalidTimeAndMixedProceduresInEitherOrder) { for (const std::string row : {"1, 100,,,,", "1, 100, 0", "1, 100,, 2", "1", "1,", "0, 1", "-1, 1", "1, 0", "1, -1", "nan, 1", "1, inf", "1e-300, 1e300", "1, 100\n1, 100"}) ExpectRejected(Replace(ThermalDeck(), "1., 100.", row), "HEAT TRANSFER"); for (const std::string option : {"steady state", "deltmx=1", "end=SS", "unknown=1"}) ExpectRejected( Replace(ThermalDeck(), "*Heat Transfer", "*Heat Transfer, " + option), "HEAT TRANSFER"); for (const std::string option : {"inc=0", "inc=-1", "inc=4294967296", "inc=1.5", "inc="}) ExpectRejected(Replace(ThermalDeck(), "inc=100", option), "STEP"); for (const std::string procedure : {"*Static\n1, 1, 1, 1\n", "*Frequency\n1\n", "*Dynamic, direct\n1, 100\n", "*Heat Transfer\n1, 100\n"}) { ExpectRejected(Replace(ThermalDeck(), "*Heat Transfer\n", procedure + "*Heat Transfer\n")); ExpectRejected( Replace(ThermalDeck(), "1., 100.\n", "1., 100.\n" + procedure)); } ExpectRejected(ThermalDeck() + "*Step\n*Heat Transfer\n1,1\n*End Step\n", "STEP"); } TEST(ThermalDomainMapping, ThermalMaterialAndSourceProvenanceArePreserved) { const auto deck = Replace( ThermalDeck(), "*Material, name=M", "*Material, name=Unused\n*Density\n2\n*Conductivity\n3\n*Specific " "Heat\n4\n*Material, name=M"); const TemporaryInput input{deck}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_TRUE(result.HasValue()); const auto& domain = result.Value(); ASSERT_EQ(domain.ThermalMaterials().Size(), 2U); ASSERT_EQ(domain.ThermalSections().size(), 1U); const auto& section = domain.ThermalSections()[0]; EXPECT_EQ(section.material_index, 1U); EXPECT_EQ(section.name, "E"); EXPECT_EQ(section.source_id.source_label_text, "E"); EXPECT_EQ(section.location.file, input.Path()); EXPECT_EQ(section.location.line, LineOf(deck, "*Shell Section")); EXPECT_DOUBLE_EQ(section.thickness, .5); const auto& material = domain.ThermalMaterials()[section.material_index]; EXPECT_EQ(material.Name(), "M"); EXPECT_DOUBLE_EQ(material.Conductivity(), 54.); EXPECT_DOUBLE_EQ(material.Density(), 7850.); EXPECT_DOUBLE_EQ(material.SpecificHeat(), 460.); EXPECT_EQ(material.Location().line, LineOf(deck, "*Material, name=M")); EXPECT_EQ(domain.ThermalElements()[0].SourceId().instance_name, "I"); EXPECT_EQ(domain.ThermalElements()[0].SourceId().source_label, 1); EXPECT_EQ(domain.ThermalStep()->name, "Heat"); EXPECT_EQ(domain.ThermalStep()->location.file, input.Path()); EXPECT_EQ(domain.ThermalStep()->location.line, LineOf(deck, "*Step")); } TEST(ThermalDomainMapping, FiniteThermalScalarsPreserveSubnormalAndSignedValues) { const double tiny = std::numeric_limits::denorm_min(); const std::string text = Number(tiny); for (const auto& item : std::vector>{ {"54.,", text}, {"7850.,", text}, {"460.,", text}, {".5, 5", text + ",5"}, {"Bottom, 20.", "Bottom, -" + text}, {"Bottom, 15, 15, 30.", "Bottom, 15, 15, -" + text}, {"Bottom, 11, -2.", "Bottom, 11, -" + text}}) { SCOPED_TRACE(item.first); const auto result = MapText(Replace(ThermalDeck(), item.first, item.second)); ASSERT_TRUE(result.HasValue()); const auto& domain = result.Value(); if (item.first == "54.,") EXPECT_EQ(domain.ThermalMaterials()[0].Conductivity(), tiny); if (item.first == "7850.,") EXPECT_EQ(domain.ThermalMaterials()[0].Density(), tiny); if (item.first == "460.,") EXPECT_EQ(domain.ThermalMaterials()[0].SpecificHeat(), tiny); if (item.first == ".5, 5") EXPECT_EQ(domain.ThermalSections()[0].thickness, tiny); if (item.first == "Bottom, 20.") EXPECT_EQ(domain.ThermalInitialConditions()[0].station_values[4], -tiny); if (item.first == "Bottom, 15, 15, 30.") EXPECT_EQ(domain.ThermalStep()->Boundaries()[0].value, -tiny); if (item.first == "Bottom, 11, -2.") EXPECT_EQ(domain.ThermalStep()->Loads()[0].power, -tiny); } for (const std::string value : {"+2.5", "-0", "0", "-20.25", "1.7976931348623157e308"}) { const auto result = MapText(Replace(ThermalDeck(), "Bottom, 20.", "Bottom, " + value)); ASSERT_TRUE(result.HasValue()); } for (const std::string value : {"1e-999", "1e999", "0x1p0", "+-1", "1D2"}) { for (const std::string original : {"54.,", "460.,", ".5, 5", "Bottom, 20.", "Bottom, 11, -2."}) { const auto replacement = original == "Bottom, 20." ? "Bottom, " + value : (original == "Bottom, 11, -2." ? "Bottom, 11, " + value : value); ExpectRejected(Replace(ThermalDeck(), original, replacement)); } } } TEST(ThermalDomainMapping, ThermalSetAndSurfaceDuplicatesExpandOnce) { auto deck = Replace(ThermalDeck(), "*Elset, elset=E\n1\n", "*Elset, elset=E\n1,1\n"); deck = Replace( deck, "*End Assembly", "*Surface, type=ELEMENT, name=S\nI.1, SPOS\nI.1, SPOS\n*End Assembly"); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()); EXPECT_EQ(result.Value().ThermalElements().Size(), 1U); ASSERT_EQ(result.Value().ElementSurfaces().size(), 1U); EXPECT_EQ(result.Value().ElementSurfaces()[0].facets.size(), 1U); EXPECT_EQ(result.Value().Warnings().size(), 1U); ExpectRejected(Replace(deck, "I.1, SPOS\nI.1, SPOS", "I.1, SPOS\nI.1, SNEG"), "SURFACE"); } TEST(ThermalDomainMapping, DeferredFailuresRetainExactInputLocationAndIdentity) { auto deck = Replace(ThermalDeck(), "inc=100", "inc=99"); const TemporaryInput input{deck}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_FALSE(result.HasValue()); const auto& diagnostic = result.GetStatus().Diagnostics()[0]; EXPECT_EQ(diagnostic.location.file, input.Path()); EXPECT_EQ(diagnostic.location.line, LineOf(deck, "1., 100.")); EXPECT_EQ(diagnostic.keyword, "HEAT TRANSFER"); EXPECT_EQ(diagnostic.entity_identity, "Heat"); auto preprint = std::string{"*Preprint, echo=NO, model=NO, history=NO\n"} + ThermalDeck(); const auto missing_contact = MapText(preprint); ASSERT_FALSE(missing_contact.HasValue()); EXPECT_EQ(missing_contact.GetStatus().Diagnostics()[0].location.line, 1U); EXPECT_EQ(missing_contact.GetStatus().Diagnostics()[0].keyword, "PREPRINT"); } TEST(ThermalDomainMapping, RejectsMixedElementsAndWrongThermalMaterialScopes) { ExpectRejected(Replace(ThermalDeck(), "*Elset, elset=E", "*Element, type=S4\n2,1,2,3,4\n*Elset, elset=E"), "ELEMENT"); for (const std::string block : {"*Conductivity\n54\n", "*Specific Heat\n460\n", "*Initial Conditions, type=TEMPERATURE\n1, 1\n"}) { ExpectRejected(Replace(ThermalDeck(), "*End Part", block + "*End Part")); ExpectRejected( Replace(ThermalDeck(), "*End Assembly", block + "*End Assembly")); ExpectRejected(Replace(ThermalDeck(), "*End Step", block + "*End Step")); } for (const std::string property : {"*Conductivity\n54.,\n", "*Specific Heat\n460.,\n"}) { auto deck = Replace(ThermalDeck(), property, ""); deck = Replace(deck, "*Material", property + "*Material"); ExpectRejected(deck); } } TEST(ThermalDomainMapping, InlineElementSetResolvesSectionCaseInsensitively) { auto deck = Replace(ThermalDeck(), "*Element, type=DS4", "*Element, type=ds4, elset=inlineShells"); deck = Replace(deck, "*Elset, elset=E\n1\n", ""); deck = Replace(deck, "*Shell Section, elset=E", "*Shell Section, elset=INLINESHELLS"); const TemporaryInput input{deck}; const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path()); ASSERT_TRUE(parsed.HasValue()); const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value()); ASSERT_TRUE(result.HasValue()) << result.GetStatus().Diagnostics().front().message; const auto& domain = result.Value(); ASSERT_EQ(domain.ElementSets().size(), 1U); const auto& set = domain.ElementSets()[0]; EXPECT_EQ(set.name, "inlineShells"); EXPECT_EQ(set.instance_name, "I"); EXPECT_EQ(set.element_indices, std::vector{0U}); EXPECT_EQ(set.location.file, input.Path()); EXPECT_EQ(set.location.line, LineOf(deck, "*Element")); ASSERT_EQ(domain.ThermalElements().Size(), 1U); EXPECT_EQ(domain.ThermalElements()[0].ThermalSectionIndex(), 0U); EXPECT_EQ(domain.ThermalSections()[0].name, "INLINESHELLS"); EXPECT_EQ(domain.ThermalSections()[0].location.line, LineOf(deck, "*Shell Section")); } TEST(ThermalDomainMapping, InlineElementSetPreservesElementRowOrder) { auto deck = Replace(ThermalDeck(), "*Element, type=DS4\n1, 1, 2, 3, 4", "5,2,0,0\n6,3,0,0\n7,3,1,0\n8,2,1,0\n" "*Element, type=DS4, elset=E\n7,1,2,3,4\n3,5,6,7,8"); deck = Replace(deck, "*Elset, elset=E\n1\n", ""); const auto result = MapText(deck); ASSERT_TRUE(result.HasValue()) << result.GetStatus().Diagnostics().front().message; const auto& domain = result.Value(); ASSERT_EQ(domain.ElementSets().size(), 1U); ASSERT_EQ(domain.ElementSets()[0].element_indices.size(), 2U); EXPECT_EQ(domain.ElementSets()[0].element_indices, (std::vector{0U, 1U})); EXPECT_EQ(domain.ThermalElements()[0].SourceId().source_label, 7); EXPECT_EQ(domain.ThermalElements()[1].SourceId().source_label, 3); EXPECT_EQ(domain.ThermalElements()[0].ThermalSectionIndex(), 0U); EXPECT_EQ(domain.ThermalElements()[1].ThermalSectionIndex(), 0U); } TEST(ThermalDomainMapping, InlineElementSetRejectsInvalidAndDuplicateDefinitions) { auto deck = Replace(ThermalDeck(), "*Elset, elset=E\n1\n", ""); for (const std::string option : {"elset", "elset=", "elset=E, unknown=1", "elset=E, elset=e"}) { SCOPED_TRACE(option); const auto result = MapText( Replace(deck, "*Element, type=DS4", "*Element, type=DS4, " + option)); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "ELEMENT"); EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, LineOf(deck, "*Element")); } ExpectRejected(Replace(ThermalDeck(), "*Element, type=DS4", "*Element, type=DS4, elset=e"), "ELSET"); deck = Replace(deck, "*Element, type=DS4", "*Element, type=DS4, elset=E"); ExpectRejected( Replace(deck, "*Shell Section", "*Element, type=DS4, elset=e\n2,1,2,3,4\n*Shell Section"), "ELEMENT"); auto conflicting = Replace(deck, "*Shell Section", "*Elset, elset=Other\n1\n*Shell Section"); conflicting = Replace(conflicting, "*End Part", "*Shell Section, elset=Other, material=M\n.5\n*End Part"); ExpectRejected(conflicting, "SHELL SECTION"); } TEST(ThermalDomainMapping, RejectsBeamSectionAndBeamOnlyChildrenAtTheirSource) { const std::string beam = "*Beam General Section, elset=E, material=M, " "section=GENERAL\n1,1,0,1,1\n1,0,0\n"; for (const std::string suffix : {"", "*Section Points\n0,0\n", "*Transverse Shear Stiffness\n1,1,1\n"}) { const auto deck = Replace(ThermalDeck(), "*End Part", beam + suffix + "*End Part"); const auto result = MapText(deck); ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "BEAM GENERAL SECTION"); EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, LineOf(deck, "*Beam General Section")); } ExpectRejected( Replace(ThermalDeck(), "*End Part", "*Section Points\n0,0\n*End Part"), "SECTION POINTS"); ExpectRejected(Replace(ThermalDeck(), "*End Part", "*Transverse Shear Stiffness\n1,1,1\n*End Part"), "TRANSVERSE SHEAR STIFFNESS"); } TEST(ThermalDomainMapping, RejectsMalformedDs4ConnectivityAndElementOptions) { for (const std::string row : {"1,1,2,3", "1,1,2,3,4,1", "1,1,2,3,3", "1,0,2,3,4", "1,-1,2,3,4", "1,1.5,2,3,4", "1,1,2,3,99", "0,1,2,3,4", "1.5,1,2,3,4", "1,1,2,3,4\n1,1,2,3,4"}) { SCOPED_TRACE(row); ExpectRejected(Replace(ThermalDeck(), "1, 1, 2, 3, 4", row), "ELEMENT"); } ExpectRejected(Replace(ThermalDeck(), "*Element, type=DS4", "*Element, type=DS4, unknown=1"), "ELEMENT"); } } // namespace