From 3ddd77ce6c1f7264e31f40878033b747feb5f63a Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Fri, 9 Oct 2026 01:25:10 +0900 Subject: [PATCH] Reject duplicate Solid procedures and unsupported static restart --- src/fesa/io/abaqus/domain_mapping_context.cpp | 26 ++++- tests/unit/io/abaqus/hex8_semantics_test.cpp | 106 ++++++++++++++++++ 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/src/fesa/io/abaqus/domain_mapping_context.cpp b/src/fesa/io/abaqus/domain_mapping_context.cpp index 6784497..4d5b046 100644 --- a/src/fesa/io/abaqus/domain_mapping_context.cpp +++ b/src/fesa/io/abaqus/domain_mapping_context.cpp @@ -2387,6 +2387,16 @@ class DomainMappingContext::Impl { } void ParseStepBlock(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kSolid && + (block.canonical_name == "STATIC" || + block.canonical_name == "FREQUENCY" || + block.canonical_name == "DYNAMIC") && + (step_.has_static || step_.frequency || step_.dynamic)) { + InputFailure("unsupported-analysis-procedure", block.location, + block.canonical_name, step_.name_raw, + "Solid steps accept exactly one procedure."); + return; + } if (model_element_family_ == ElementFamily::kThermalShell && (block.canonical_name == "STATIC" || block.canonical_name == "FREQUENCY" || @@ -2525,17 +2535,16 @@ class DomainMappingContext::Impl { } step_no_op_seen_ = true; if (step_.frequency || step_.has_heat_transfer || - (step_.dynamic && model_element_family_ == ElementFamily::kSolid)) { + model_element_family_ == ElementFamily::kSolid) { const auto* write = Parameter(block, "WRITE"); const auto* frequency = Parameter(block, "FREQUENCY"); if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) || write == nullptr || write->value || frequency == nullptr || !frequency->value || *frequency->value != "0") { if (!failure_) { - InputFailure("unsupported-keyword", block.location, - block.canonical_name, "", - "Dynamic and frequency RESTART accept only WRITE, " - "FREQUENCY=0."); + InputFailure( + "unsupported-keyword", block.location, block.canonical_name, "", + "Supported RESTART no-op requires WRITE, FREQUENCY=0."); } return; } @@ -2900,8 +2909,13 @@ class DomainMappingContext::Impl { if (model_element_family_ == ElementFamily::kSolid && Parameter(block, "DIRECT")) { if (step_.perturbation_explicit || step_.has_static || step_.frequency || - !ValidateParameters(block, {"DIRECT"})) + step_.dynamic) { + InputFailure("unsupported-analysis-procedure", block.location, + block.canonical_name, step_.name_raw, + "STATIC DIRECT requires one non-perturbation procedure."); return; + } + if (!ValidateParameters(block, {"DIRECT"})) return; const auto* direct = Parameter(block, "DIRECT"); if (direct->value || block.data.size() != 1U || (block.data[0].fields.size() < 2U || diff --git a/tests/unit/io/abaqus/hex8_semantics_test.cpp b/tests/unit/io/abaqus/hex8_semantics_test.cpp index 4308888..c45c7bd 100644 --- a/tests/unit/io/abaqus/hex8_semantics_test.cpp +++ b/tests/unit/io/abaqus/hex8_semantics_test.cpp @@ -446,4 +446,110 @@ TEST(Hex8Semantics, RejectsInvalidProgrammaticSolidCandidates) { 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