From ba57a469c7ab5d59274b96b16d600183a127a5b7 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Tue, 29 Sep 2026 20:18:00 +0900 Subject: [PATCH] fix: tighten thermal input diagnostics --- src/fesa/io/abaqus/domain_mapping_context.cpp | 33 ++++++++++++++++--- tests/unit/io/abaqus/thermal_domain_test.cpp | 17 ++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/fesa/io/abaqus/domain_mapping_context.cpp b/src/fesa/io/abaqus/domain_mapping_context.cpp index 9f1cc89..11b636b 100644 --- a/src/fesa/io/abaqus/domain_mapping_context.cpp +++ b/src/fesa/io/abaqus/domain_mapping_context.cpp @@ -1639,6 +1639,11 @@ class DomainMappingContext::Impl { } void ParseElastic(const KeywordBlock& block) { + if (model_element_family_ == ElementFamily::kThermalShell) { + InputFailure("unsupported-keyword", block.location, block.canonical_name, + "", "ELASTIC is outside the thermal material subset."); + return; + } if (!material_eligible_) { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", @@ -2564,6 +2569,13 @@ class DomainMappingContext::Impl { "HEAT TRANSFER accepts only END=PERIOD."); return; } + if (block.data[0].fields.size() > 5U) { + InputFailure( + "invalid-thermal-time-grid", block.data[0].location, + block.canonical_name, "", + "HEAT TRANSFER permits at most three trailing empty fields."); + return; + } const auto fields = WithoutTrailingEmpty(block.data[0].fields); if (fields.size() != 2U) { InputFailure("invalid-thermal-time-grid", block.data[0].location, @@ -2631,8 +2643,9 @@ class DomainMappingContext::Impl { "PERTURBATION is supported only with FREQUENCY."); return; } - if (step_.has_static || !ValidateParameters(block, {}) || - block.data.size() != 1U || block.data[0].fields.size() != 4U) { + if (step_.has_heat_transfer || step_.has_static || + !ValidateParameters(block, {}) || block.data.size() != 1U || + block.data[0].fields.size() != 4U) { if (!failure_) { InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data" : "invalid-static-data", @@ -2740,6 +2753,12 @@ class DomainMappingContext::Impl { } void ParseOutputChild(const KeywordBlock& block) { + if (step_.has_heat_transfer) { + InputFailure("unsupported-keyword", block.location, block.canonical_name, + "", + "Thermal output children are outside the approved subset."); + return; + } if (!active_output_) { InputFailure( "invalid-keyword-location", block.location, block.canonical_name, "", @@ -2918,10 +2937,14 @@ class DomainMappingContext::Impl { if (model_element_family_ == ElementFamily::kThermalShell) { for (const auto& block : input_.blocks) { if (block.canonical_name != "PREPRINT") continue; - if (!ValidateParameters(block, - {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || - block.parameters.size() != 4U) + if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) return; + if (block.parameters.size() != 4U) { + InputFailure("unsupported-keyword", block.location, + block.canonical_name, "", + "Thermal PREPRINT requires four NO parameters."); + return; + } for (const auto& parameter : block.parameters) { if (!parameter.value || !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { diff --git a/tests/unit/io/abaqus/thermal_domain_test.cpp b/tests/unit/io/abaqus/thermal_domain_test.cpp index 90bb15f..1748c4c 100644 --- a/tests/unit/io/abaqus/thermal_domain_test.cpp +++ b/tests/unit/io/abaqus/thermal_domain_test.cpp @@ -147,6 +147,7 @@ TEST(ThermalDomainMapping, RejectsThermalMaterialAndControlForms) { {"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"}, @@ -163,6 +164,22 @@ TEST(ThermalDomainMapping, RejectsThermalMaterialAndControlForms) { } } +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.");