#include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/assembly/load_assembler.h" #include "fesa/assembly/parallel_for.h" #include "fesa/assembly/sparse_assembler.h" #include "fesa/fem/dof_manager.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" namespace { std::string Deck(const std::string& procedure = "*Static\n1,1,1,1\n", const std::string& boundary = "10,1,3\n", const std::string& extra = "") { return "*Part,name=P\n*Node\n10,0,0,0\n20,1,0,0\n30,1,1,0\n40,0,1,0\n" "50,0,0,1\n60,1,0,1\n70,1,1,1\n80,0,1,1\n" "*Element,type=C3D8,elset=E\n99,10,20,30,40,50,60,70,80\n" "*Solid Section,elset=E,material=M\n,\n*End Part\n" "*Assembly,name=A\n*Instance,name=I,part=P\n*End Instance\n" "*End Assembly\n*Material,name=M\n*Elastic\n1000,.3\n" + extra + "*Step,name=Solid,inc=100\n" + procedure + (boundary.empty() ? "" : "*Boundary\n" + boundary) + "*End Step\n"; } fesa::Result Map(const std::string& text) { const auto path = std::filesystem::temp_directory_path() / "fesa-hex8-semantics.inp"; { std::ofstream out{path}; out << text; } auto syntax = fesa::AbaqusInputReader{}.Read(path); std::filesystem::remove(path); if (!syntax.HasValue()) return fesa::Result::Failure(syntax.GetStatus()); return fesa::AbaqusDomainMapper{}.Map(syntax.Value()); } TEST(Hex8Semantics, MapsEightSourceNodesAndExactlyThreePhysicalDofs) { auto domain = Map(Deck()); ASSERT_TRUE(domain.HasValue()) << (domain.GetStatus().Diagnostics().empty() ? "" : domain.GetStatus().Diagnostics()[0].message); ASSERT_EQ(domain.Value().Elements().Size(), 1U); EXPECT_EQ(domain.Value().Elements()[0].SourceElementType(), "C3D8"); EXPECT_EQ(domain.Value().Elements()[0].SourceId().source_label, 99); EXPECT_EQ(domain.Value().Elements()[0].NodeIndices().size(), 8U); auto model = fesa::AnalysisModel::Create(domain.Value()); ASSERT_TRUE(model.HasValue()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()); EXPECT_EQ(dofs.Value().FullDofCount(), 24U); EXPECT_EQ(dofs.Value().ConstrainedDofCount(), 3U); } TEST(Hex8Semantics, InactiveZeroRotationsWarnAndDoNotCreateConstraints) { auto domain = Map(Deck("*Static\n1,1,1,1\n", "10,1,6\n")); ASSERT_TRUE(domain.HasValue()) << (domain.GetStatus().Diagnostics().empty() ? "" : domain.GetStatus().Diagnostics()[0].message); EXPECT_EQ(domain.Value().Steps()[0].BoundaryConditions().size(), 3U); ASSERT_FALSE(domain.Value().Warnings().empty()); EXPECT_EQ(domain.Value().Warnings().back().keyword, "BOUNDARY"); EXPECT_EQ(domain.Value().Warnings().back().entity_identity, "10"); EXPECT_GT(domain.Value().Warnings().back().location.line, 0U); } TEST(Hex8Semantics, RejectsNonzeroRotationsAndAllRotationalLoads) { EXPECT_FALSE(Map(Deck("*Static\n1,1,1,1\n", "10,4,4,1\n")).HasValue()); EXPECT_FALSE( Map(Deck("*Static\n1,1,1,1\n", "10,1,3\n*Cload\n20,4,0\n")).HasValue()); } std::string Replace(std::string input, const std::string& from, const std::string& to) { const auto position = input.find(from); if (position == std::string::npos) throw std::logic_error{"Missing mutation source"}; input.replace(position, from.size(), to); return input; } TEST(Hex8Semantics, RotationNoOpPreservesPhysicalMapAndLoads) { auto physical = Map(Deck("*Static\n1,1,1,1\n", "10,1,3,0\n*Cload\n20,2,17\n")); auto extra = Map(Deck("*Static\n1,1,1,1\n", "10,1,6,0\n20,4,6,-0\n*Cload\n20,2,17\n")); ASSERT_TRUE(physical.HasValue()); ASSERT_TRUE(extra.HasValue()); auto pm = fesa::AnalysisModel::Create(physical.Value()); auto em = fesa::AnalysisModel::Create(extra.Value()); auto pd = fesa::DofManager::Create(pm.Value()); auto ed = fesa::DofManager::Create(em.Value()); ASSERT_TRUE(pd.HasValue()); ASSERT_TRUE(ed.HasValue()); EXPECT_EQ(pd.Value().FreeDofs(), ed.Value().FreeDofs()); EXPECT_EQ(pd.Value().ConstrainedDofs(), ed.Value().ConstrainedDofs()); EXPECT_EQ(pd.Value().GetSparsePattern().column_indices, ed.Value().GetSparsePattern().column_indices); EXPECT_EQ(ed.Value().Profile(), fesa::DofProfile::kSolidStructural); EXPECT_EQ(ed.Value().ComponentsPerNode(), 3U); EXPECT_THROW(ed.Value().FullDof(0, fesa::DofComponent::kUrx), std::out_of_range); const auto& element = extra.Value().Elements()[0]; auto scatter = ed.Value().ElementScatter( {element.SourceId(), element.NodeIndices(), {fesa::DofComponent::kUx, fesa::DofComponent::kUy, fesa::DofComponent::kUz}}); ASSERT_TRUE(scatter.HasValue()); EXPECT_EQ(scatter.Value().size(), 24U); EXPECT_EQ(scatter.Value().front(), 0U); EXPECT_EQ(scatter.Value().back(), 23U); auto pf = fesa::LoadAssembler::AssembleFullNodalLoad(pm.Value(), pd.Value()); auto ef = fesa::LoadAssembler::AssembleFullNodalLoad(em.Value(), ed.Value()); ASSERT_TRUE(pf.HasValue()); ASSERT_TRUE(ef.HasValue()); for (std::size_t i = 0; i < 24U; ++i) EXPECT_EQ(pf.Value()[i], ef.Value()[i]); } TEST(Hex8Semantics, RejectsTinyRotationBadTargetsOptionsAndDofRanges) { for (const std::string row : {"10,4,6,1e-100\n", "Missing,4,6,0\n", "10,0,3\n", "10,1,7\n"}) EXPECT_FALSE(Map(Deck("*Static\n1,1,1,1\n", row)).HasValue()) << row; for (const std::string option : {"type=VELOCITY", "amplitude=A", "op=NEW"}) EXPECT_FALSE( Map(Replace(Deck(), "*Boundary", "*Boundary," + option)).HasValue()); for (const std::string row : {"10,4,6\n", "10,4,6,0\n", "10,4,6,\n"}) { auto result = Map(Deck("*Static\n1,1,1,1\n", row)); ASSERT_TRUE(result.HasValue()) << row; EXPECT_TRUE(result.Value().Steps()[0].BoundaryConditions().empty()); ASSERT_EQ(result.Value().Warnings().size(), 1U); EXPECT_EQ(result.Value().Warnings()[0].code, "inactive-solid-rotation-boundary"); EXPECT_EQ(result.Value().Warnings()[0].severity, fesa::Severity::kWarning); } } TEST(Hex8Semantics, AllowsEmptyFreeAndNoBoundaryModels) { std::string all; for (int node = 10; node <= 80; node += 10) all += std::to_string(node) + ",1,6\n"; for (const auto& boundaries : {all, std::string{}}) { auto domain = Map(Deck("*Static\n1,1,1,1\n", boundaries)); ASSERT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()); EXPECT_EQ(dofs.Value().FullDofCount(), 24U); EXPECT_EQ(dofs.Value().FreeDofCount(), boundaries.empty() ? 24U : 0U); } } TEST(Hex8Semantics, RetainsStaticDensityTargetsAndDirectSchedule) { auto domain = Map(Deck("*Static,direct\n.1,1,\n", "10,1,3,.25\n", "*Density\n2\n")); ASSERT_TRUE(domain.HasValue()) << (domain.GetStatus().Diagnostics().empty() ? "" : domain.GetStatus().Diagnostics()[0].message); EXPECT_TRUE(domain.Value().IsSolid()); ASSERT_EQ(domain.Value().SolidSections().Size(), 1U); EXPECT_EQ(domain.Value().SolidSections()[0].MaterialIndex(), 0U); EXPECT_EQ(domain.Value().Densities()[0].rho, 2.0); const auto& step = domain.Value().Steps()[0]; EXPECT_EQ(step.Name(), "Solid"); EXPECT_TRUE(step.IsDirectStatic()); EXPECT_EQ(step.IncrementCount(), 10U); EXPECT_EQ(step.MaximumIncrements(), 100U); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); EXPECT_EQ(dofs.Value().PrescribedValues()[0], .25); for (const std::string values : {".3,1\n", "0,1\n", ".1,11\n"}) EXPECT_FALSE(Map(Deck("*Static,direct\n" + values)).HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "1000,.3", "1000,.5")).HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "1000,.3", "1000,-1")).HasValue()); } TEST(Hex8Semantics, SamplesIndependentAmplitudeLoadsAtActualEndpoints) { const std::string amplitudes = "*Density\n2\n*Amplitude,name=A\n0,0,.1,1\n10,1\n" "*Amplitude,name=B,definition=TABULAR,time=STEP " "TIME,value=RELATIVE\n0,2,1,4\n"; const auto text = Deck("*Dynamic,direct\n.1,10,\n", "10,1,6\n*Cload,amplitude=a\n20,1,10\n" "*Cload,amplitude=B\n20,1,3\n*Cload\n20,1,5\n", amplitudes); auto domain = Map(text); ASSERT_TRUE(domain.HasValue()) << (domain.GetStatus().Diagnostics().empty() ? "" : domain.GetStatus().Diagnostics()[0].message); ASSERT_EQ(domain.Value().Amplitudes().size(), 2U); const auto& amplitude = domain.Value().Amplitudes()[0]; EXPECT_EQ(amplitude.name_raw, "A"); EXPECT_EQ(amplitude.Evaluate(0), 0); EXPECT_EQ(amplitude.Evaluate(.05), .5); EXPECT_EQ(amplitude.Evaluate(.1), 1); EXPECT_EQ(amplitude.Evaluate(20), 1); EXPECT_FALSE(domain.Value().Dynamic()->nlgeom); EXPECT_EQ(domain.Value().Dynamic()->increment_count, 100U); const auto& loads = domain.Value().Steps()[0].ConcentratedLoads(); EXPECT_EQ(loads[0].AmplitudeIndex(), std::optional{0U}); EXPECT_EQ(loads[1].AmplitudeIndex(), std::optional{1U}); EXPECT_EQ(loads[0].AmplitudeName(), "a"); EXPECT_GT(loads[0].AmplitudeLocation().line, 0U); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()); for (double time : {0., .05, .1, 1., 10.}) { auto load = fesa::LoadAssembler::AssembleFullNodalLoad(model.Value(), dofs.Value(), time); ASSERT_TRUE(load.HasValue()); EXPECT_DOUBLE_EQ(load.Value()[3], amplitude.Evaluate(time) * 10 + domain.Value().Amplitudes()[1].Evaluate(time) * 3 + 5); } EXPECT_FALSE(Map(Replace(text, "10,1,6", "10,1,3,1e-100")).HasValue()); EXPECT_FALSE( Map(Replace(text, "amplitude=a", "amplitude=missing")).HasValue()); EXPECT_TRUE( Map(Replace(text, "name=Solid,inc=100", "name=Solid,nlgeom=YES,inc=100")) .HasValue()); } TEST(Hex8Semantics, RejectsAmplitudeOptionsDataAndCrossFamilyUse) { const std::string prefix = "*Density\n2\n"; for (const std::string amplitude : {"*Amplitude,name=A,time=TOTAL TIME\n0,0\n", "*Amplitude,name=A,definition=SMOOTH STEP\n0,0\n", "*Amplitude,name=A,value=ABSOLUTE\n0,0\n", "*Amplitude,name=A,smooth=0\n0,0\n", "*Amplitude,name=A\n0,0,1\n", "*Amplitude,name=A\n0,0,0,1\n", "*Amplitude,name=A\n1,0,0,1\n", "*Amplitude,name=A\n-1,0\n", "*Amplitude,name=A\n0,nan\n", "*Amplitude,name=A\n", "*Amplitude,name=A\n0,0\n*Amplitude,name=a\n0,1\n"}) EXPECT_FALSE( Map(Deck("*Dynamic,direct\n.1,1\n", "10,1,3\n", prefix + amplitude)) .HasValue()) << amplitude; EXPECT_FALSE( Map(Deck("*Static\n1,1,1,1\n", "10,1,3\n", "*Amplitude,name=A\n0,0\n")) .HasValue()); EXPECT_FALSE(Map(Deck("*Dynamic,direct\n.1,1\n", "10,1,3\n")).HasValue()); } TEST(Hex8Semantics, RejectsAliasesConnectivityThicknessAndMixedModels) { for (const std::string type : {"C3D8R", "C3D8I", "C3D8H", "C3D20"}) EXPECT_FALSE(Map(Replace(Deck(), "type=C3D8", "type=" + type)).HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "99,10,20,30,40,50,60,70,80", "99,10,20,30,40,50,60,70,70")) .HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "99,10,20,30,40,50,60,70,80", "99,10,20,30,40,50,60,70,999")) .HasValue()); EXPECT_FALSE( Map(Replace(Deck(), "material=M\n,", "material=M\n2")).HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "*Solid Section", "*Element,type=S4\n100,10,20,30,40\n*Solid Section")) .HasValue()); } TEST(Hex8Semantics, PreservesShellSixComponentProfile) { auto text = Replace(Deck(), "50,0,0,1\n60,1,0,1\n70,1,1,1\n80,0,1,1\n", ""); text = Replace(text, "*Element,type=C3D8,elset=E\n99,10,20,30,40,50,60,70,80", "*Element,type=S4\n99,10,20,30,40\n*Elset,elset=E\n99"); text = Replace(text, "*Solid Section,elset=E,material=M\n,", "*Shell Section,elset=E,material=M\n.1"); auto domain = Map(text); ASSERT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()); EXPECT_EQ(dofs.Value().Profile(), fesa::DofProfile::kStructural); EXPECT_EQ(dofs.Value().ComponentsPerNode(), 6U); EXPECT_EQ(dofs.Value().FullDofCount(), 24U); EXPECT_EQ(dofs.Value().FullDof(1, fesa::DofComponent::kUrx), 9U); } TEST(Hex8Semantics, MapsAllFiveUnmodifiedRequiredSourceDecks) { auto root = std::filesystem::path{__FILE__}.parent_path(); for (int i = 0; i < 4; ++i) root = root.parent_path(); for (const std::string name : {"hexa_linearstatic.inp", "hexa_nonlinearstatic.inp", "hexa_freq.inp", "hexa_dynlinear.inp", "hexa_dynnonlinear.inp"}) { auto syntax = fesa::AbaqusInputReader{}.Read(root / "reference/hexa" / name); ASSERT_TRUE(syntax.HasValue()) << name; auto domain = fesa::AbaqusDomainMapper{}.Map(syntax.Value()); ASSERT_TRUE(domain.HasValue()) << name << ": " << (domain.GetStatus().Diagnostics().empty() ? "" : domain.GetStatus().Diagnostics()[0].message); EXPECT_EQ(domain.Value().SolidElements().Size(), 300U); EXPECT_EQ(domain.Value().Nodes().size(), 442U); EXPECT_EQ(std::count_if(domain.Value().Warnings().begin(), domain.Value().Warnings().end(), [](const auto& warning) { return warning.code == "inactive-solid-rotation-boundary"; }), 3); auto model = fesa::AnalysisModel::Create(domain.Value()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()) << name; EXPECT_EQ(dofs.Value().FullDofCount(), 1326U); EXPECT_EQ(dofs.Value().ConstrainedDofCount(), 51U); } } class ConstantMatrixElement final : public fesa::Element { public: ConstantMatrixElement(const fesa::ElementDefinition& definition, double entry) : layout_{definition.SourceId(), definition.NodeIndices(), {fesa::DofComponent::kUx, fesa::DofComponent::kUy, fesa::DofComponent::kUz}}, entry_{entry} {} const fesa::ElementDofLayout& DofLayout() const noexcept override { return layout_; } fesa::Result ComputeStiffness() const override { fesa::Matrix values{24U, 24U}; for (std::size_t row = 0U; row < 24U; ++row) for (std::size_t column = 0U; column < 24U; ++column) values(row, column) = entry_; return fesa::Result::Success( {layout_, std::move(values)}); } fesa::Result Recover( const fesa::Vector&) const override { return fesa::Result::Failure( fesa::Status::Failure(fesa::FailureCategory::kModel, {{fesa::Severity::kError, "test-no-recovery", {}, "", "", "Assembly fixture has no recovery."}})); } private: fesa::ElementDofLayout layout_; double entry_; }; TEST(Hex8Semantics, AssemblesFullSymmetricSolidMatricesInStableSourceOrder) { const auto text = Replace(Deck(), "99,10,20,30,40,50,60,70,80\n", "99,10,20,30,40,50,60,70,80\n100,10,20,30,40,50,60,70,80\n" "101,10,20,30,40,50,60,70,80\n"); auto domain = Map(text); ASSERT_TRUE(domain.HasValue()); auto model = fesa::AnalysisModel::Create(domain.Value()); ConstantMatrixElement first{domain.Value().Elements()[0], 1e16}; ConstantMatrixElement second{domain.Value().Elements()[1], -1e16}; ConstantMatrixElement third{domain.Value().Elements()[2], 1}; const fesa::ElementView elements{std::cref(first), std::cref(second), std::cref(third)}; fesa::DofManager dofs; ASSERT_TRUE(dofs.Build(model.Value(), elements).IsOk()); auto serial = fesa::SparseAssembler::Assemble(elements, dofs, fesa::SerialParallelFor{}); auto parallel = fesa::SparseAssembler::Assemble(elements, dofs, fesa::TbbParallelFor{}); ASSERT_TRUE(serial.HasValue()); ASSERT_TRUE(parallel.HasValue()); EXPECT_EQ(serial.Value().Rows(), 24U); EXPECT_EQ(serial.Value().Columns(), 24U); EXPECT_EQ(serial.Value().Values().size(), 576U); for (double value : serial.Value().Values()) EXPECT_EQ(value, 1.0); EXPECT_EQ(serial.Value().RowOffsets(), parallel.Value().RowOffsets()); EXPECT_EQ(serial.Value().ColumnIndices(), parallel.Value().ColumnIndices()); EXPECT_EQ(serial.Value().Values(), parallel.Value().Values()); } TEST(Hex8Semantics, ValidatesOutputNoOpsAndRejectsUnapprovedDynamicChildren) { for (const std::string output : {"*Output,field,variable=PRESELECT\n*Node Output\nU,RF\n*Element " "Output,directions=YES\nS,E\n", "*Output,history,variable=PRESELECT\n"}) EXPECT_TRUE( Map(Replace(Deck(), "*End Step", output + "*End Step")).HasValue()); EXPECT_FALSE(Map(Replace(Deck(), "*End Step", "*Output,field,variable=OTHER\n*End Step")) .HasValue()); EXPECT_FALSE( Map(Replace( Deck(), "*End Step", "*Output,field\n*Element Output,position=NODES\nS\n*End Step")) .HasValue()); auto dynamic = Deck("*Dynamic,direct\n.1,1\n", "10,1,3\n", "*Density\n2\n"); EXPECT_FALSE( Map(Replace( dynamic, "*End Step", "*Output,field,variable=PRESELECT\n*Node Output\nU\n*End Step")) .HasValue()); EXPECT_FALSE(Map(Replace(dynamic, "*End Step", "*Restart,write,frequency=1\n*End Step")) .HasValue()); } fesa::ModelDefinition SolidDefinition() { fesa::ModelDefinition definition; const std::array, 8> coordinates{{{0, 0, 0}, {1, 0, 0}, {1, 1, 0}, {0, 1, 0}, {0, 0, 1}, {1, 0, 1}, {1, 1, 1}, {0, 1, 1}}}; for (std::size_t i = 0; i < coordinates.size(); ++i) definition.nodes.push_back({{"I", static_cast(10 * (i + 1)), std::to_string(10 * (i + 1))}, coordinates[i], {"solid.inp", i + 1}}); definition.materials.emplace_back("M", 1000, .3, fesa::SourceLocation{"solid.inp", 20}); definition.solid_sections.emplace_back(fesa::SourceEntityId{"P", 1, "E"}, "E", 0, fesa::SourceLocation{"solid.inp", 21}); definition.solid_elements.emplace_back( fesa::SourceEntityId{"I", 99, "99"}, std::array{{0, 1, 2, 3, 4, 5, 6, 7}}, 0, 0, fesa::SourceLocation{"solid.inp", 22}); definition.steps.emplace_back( "Solid", std::vector{}, std::vector{}, 1., 1., 1., 1., fesa::SourceLocation{"solid.inp", 23}); return definition; } TEST(Hex8Semantics, RejectsInvalidProgrammaticSolidCandidates) { EXPECT_TRUE(fesa::Domain::Create(SolidDefinition()).HasValue()); auto repeated = SolidDefinition(); repeated.solid_elements[0].node_indices[7] = 0; EXPECT_FALSE(fesa::Domain::Create(std::move(repeated)).HasValue()); auto material = SolidDefinition(); material.materials[0].poisson_ratio = .5; EXPECT_FALSE(fesa::Domain::Create(std::move(material)).HasValue()); auto section = SolidDefinition(); section.solid_elements[0].section_index = 99; EXPECT_FALSE(fesa::Domain::Create(std::move(section)).HasValue()); auto rotational = SolidDefinition(); rotational.steps[0].loads.push_back({"10", 4, 0, {"solid.inp", 24}}); EXPECT_FALSE(fesa::Domain::Create(std::move(rotational)).HasValue()); auto unused = SolidDefinition(); unused.nodes.push_back({{"I", 90, "90"}, {2, 2, 2}, {"solid.inp", 25}}); EXPECT_FALSE(fesa::Domain::Create(std::move(unused)).HasValue()); } TEST(Hex8Semantics, RejectsSecondProcedureBeforeReadingItsOptionsOrData) { const std::array procedures{ "*Static\n1,1,1,1\n", "*Static,direct\n.1,1\n", "*Frequency\n2\n", "*Dynamic,direct\n.1,1\n"}; for (const auto& first : procedures) { ASSERT_TRUE(Map(Deck(first, "", "*Density\n2\n")).HasValue()) << first; for (const auto& second : procedures) { SCOPED_TRACE(first + second); const auto result = Map(Deck(first + second, "", "*Density\n2\n")); ASSERT_FALSE(result.HasValue()); ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].code, "unsupported-analysis-procedure"); } const auto malformed = Map(Deck(first + "*Static,direct,unapproved=YES\nnot-a-number\n", "", "*Density\n2\n")); ASSERT_FALSE(malformed.HasValue()); ASSERT_FALSE(malformed.GetStatus().Diagnostics().empty()); EXPECT_EQ(malformed.GetStatus().Diagnostics()[0].code, "unsupported-analysis-procedure"); EXPECT_EQ(malformed.GetStatus().Diagnostics()[0].keyword, "STATIC"); } } TEST(Hex8Semantics, AllowsOnlyApprovedStaticRestartNoOp) { auto approved = Map( Replace(Deck(), "*End Step", "*Restart,write,frequency=0\n*End Step")); ASSERT_TRUE(approved.HasValue()); ASSERT_EQ(approved.Value().Warnings().size(), 1U); EXPECT_EQ(approved.Value().Warnings()[0].keyword, "RESTART"); for (const std::string options : {"read", "read,step=1,inc=1", "write", "write,frequency=1", "write,frequency=0,endstep", "write,frequency=0,numberinterval=1", "write=YES,frequency=0"}) { SCOPED_TRACE(options); auto result = Map( Replace(Deck(), "*End Step", "*Restart," + options + "\n*End Step")); ASSERT_FALSE(result.HasValue()); ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "RESTART"); } } TEST(Hex8Semantics, PreservesShuffledSourceLabelsAndRejectsDuplicateIdentities) { const auto deck = Replace(Deck(), "10,0,0,0\n20,1,0,0\n30,1,1,0\n40,0,1,0\n" "50,0,0,1\n60,1,0,1\n70,1,1,1\n80,0,1,1\n", "80,0,1,1\n20,1,0,0\n60,1,0,1\n40,0,1,0\n" "10,0,0,0\n70,1,1,1\n30,1,1,0\n50,0,0,1\n"); auto domain = Map(deck); ASSERT_TRUE(domain.HasValue()); EXPECT_EQ(domain.Value().Nodes()[0].source_id.source_label, 80); EXPECT_EQ(domain.Value().Nodes()[4].source_id.source_label, 10); EXPECT_EQ(domain.Value().Elements()[0].NodeIndices(), (std::vector{4, 1, 6, 3, 7, 2, 5, 0})); auto model = fesa::AnalysisModel::Create(domain.Value()); ASSERT_TRUE(model.HasValue()); auto dofs = fesa::DofManager::Create(model.Value()); ASSERT_TRUE(dofs.HasValue()); EXPECT_EQ(dofs.Value().ConstrainedDofs(), (std::vector{12, 13, 14})); EXPECT_FALSE(Map(Replace(deck, "20,1,0,0", "80,1,0,0")).HasValue()); EXPECT_FALSE( Map(Replace(deck, "99,10,20,30,40,50,60,70,80", "99,10,20,30,40,50,60,70,80\n99,10,20,30,40,50,60,70,80")) .HasValue()); auto shuffled = SolidDefinition(); std::swap(shuffled.nodes[0], shuffled.nodes[7]); shuffled.solid_elements[0].node_indices = {7, 1, 2, 3, 4, 5, 6, 0}; auto direct = fesa::Domain::Create(std::move(shuffled)); ASSERT_TRUE(direct.HasValue()); EXPECT_EQ(direct.Value().Nodes()[0].source_id.source_label, 80); EXPECT_EQ(direct.Value().Nodes()[7].source_id.source_label, 10); EXPECT_EQ(direct.Value().SolidElements()[0].node_indices, (std::array{7, 1, 2, 3, 4, 5, 6, 0})); auto nodes = SolidDefinition(); nodes.nodes[1].source_id = nodes.nodes[0].source_id; EXPECT_FALSE(fesa::Domain::Create(std::move(nodes)).HasValue()); auto elements = SolidDefinition(); elements.solid_elements.push_back(elements.solid_elements[0]); EXPECT_FALSE(fesa::Domain::Create(std::move(elements)).HasValue()); } TEST(Hex8Semantics, RejectsUnknownSolidAssignmentTargets) { for (const std::string assignment : {"elset=Missing,material=M", "elset=E,material=Missing"}) { SCOPED_TRACE(assignment); const auto result = Map(Replace(Deck(), "elset=E,material=M", assignment)); ASSERT_FALSE(result.HasValue()); ASSERT_FALSE(result.GetStatus().Diagnostics().empty()); EXPECT_EQ(result.GetStatus().Diagnostics()[0].code, "unresolved-solid-section"); } } TEST(Hex8Semantics, RejectsInvalidSolidDynamicSchedulesAndIncrementLimits) { const auto valid = Deck("*Dynamic,direct\n.1,1\n", "", "*Density\n2\n"); ASSERT_TRUE(Map(valid).HasValue()); for (const std::string schedule : {".3,1", "0,1", "-1,1", ".1,0", "nan,1"}) { SCOPED_TRACE(schedule); EXPECT_FALSE(Map(Replace(valid, ".1,1\n", schedule + "\n")).HasValue()); } for (const std::string limit : {"0", "9", "4294967296"}) { SCOPED_TRACE(limit); EXPECT_FALSE(Map(Replace(valid, "inc=100", "inc=" + limit)).HasValue()); } } } // namespace