fix(input): validate inactive Part records

This commit is contained in:
KOKO\Mimi
2026-08-01 04:07:18 +09:00
parent af886f3f90
commit cbb621bb28
10 changed files with 383 additions and 151 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"step": 0,
"name": "abaqus-input-contract",
"status": "completed",
"summary": "Defined the normative Phase 1 Abaqus subset and expanded the public parser/mapper fixture matrix to 69 cases covering strict scopes, parameter forms, data ownership, exact mesh rows, duplicate Parts, Assembly-only hierarchical targets, and source-accurate diagnostics.",
"summary": "Defined the normative Phase 1 Abaqus subset and expanded the public parser/mapper fixture matrix to 74 cases covering strict scopes, parameter forms, data ownership, exact mesh rows and references in active and inactive Parts, duplicate Parts, Assembly-only hierarchical targets, and source-accurate diagnostics.",
"started_at": "2026-08-01T02:16:39+0900",
"completed_at": "2026-08-01T02:24:23+0900"
},
+236 -149
View File
@@ -77,18 +77,21 @@ public:
instance_name_ = std::move(selected.input->instance_name);
collect_materials();
collect_nodes();
collect_nodes(active_records_, node_ids_, true);
SetResolutionResult resolved_sets = resolve_sets(deck_);
if (!resolved_sets.diagnostics.empty()) {
diagnostics_.insert(
diagnostics_.end(),
std::make_move_iterator(resolved_sets.diagnostics.begin()),
std::make_move_iterator(resolved_sets.diagnostics.end()));
} else {
}
validate_inactive_parts(resolved_sets.sets);
if (resolved_sets.diagnostics.empty()) {
collect_resolved_sets(resolved_sets.sets);
}
collect_sections();
collect_elements();
collect_sections(active_records_, active_element_sets_, true);
collect_elements(
active_records_, node_ids_, element_ids_, true);
emit_sets();
collect_step();
@@ -318,8 +321,11 @@ private:
}
}
void collect_nodes() {
for (const DeckRecord& record : active_records_) {
void collect_nodes(
const std::span<const DeckRecord> records,
LabelMap& node_ids,
const bool emit) {
for (const DeckRecord& record : records) {
if (record.keyword != "NODE") {
continue;
}
@@ -356,7 +362,7 @@ private:
continue;
}
if (node_ids_.contains(*label)) {
if (node_ids.contains(*label)) {
add_error(
"abaqus.semantic.duplicate_node_label",
"Node label " + std::to_string(*label) +
@@ -365,17 +371,151 @@ private:
continue;
}
const NodeId id{next_node_id_++};
node_ids_.emplace(*label, id);
builder_.add_node({
id,
EntityOrigin{part_name_, instance_name_, *label},
Vec3{*x, *y, *z},
});
const NodeId id = emit
? NodeId{next_node_id_++}
: NodeId{static_cast<std::int64_t>(
node_ids.size())};
node_ids.emplace(*label, id);
if (emit) {
builder_.add_node({
id,
EntityOrigin{part_name_, instance_name_, *label},
Vec3{*x, *y, *z},
});
}
}
}
}
void validate_inactive_mesh(const ParsedPart& part) {
LabelMap node_ids;
ElementLabelMap element_ids;
collect_nodes(part.records, node_ids, false);
collect_elements(part.records, node_ids, element_ids, false);
}
struct SectionData final {
std::array<double, 5> properties{};
std::array<double, 3> orientation{};
std::optional<std::array<double, 3>> shear_values;
bool valid = true;
};
SectionData validate_section_data(
const DeckRecord& record,
const DeckRecord* shear) {
SectionData result;
if (record.data.size() != 2U ||
record.data[0].size() != result.properties.size() ||
record.data[1].size() != result.orientation.size()) {
SourceLocation source = record.source;
if (!record.data.empty()) {
source = record.data[0].size() != result.properties.size()
? data_source(record, 0U)
: data_source(
record,
record.data.size() > 2U ? 2U : 1U);
}
add_error(
"abaqus.semantic.invalid_section_data",
"*BEAM GENERAL SECTION requires exactly A, Iy, Iyz, Iz, J "
"and one three-component orientation row.",
source);
result.valid = false;
} else {
bool valid_properties = true;
for (std::size_t field = 0; field < result.properties.size();
++field) {
valid_properties =
parse_number(
record.data[0][field], result.properties[field]) &&
valid_properties;
}
valid_properties =
valid_properties && result.properties[0] > 0.0 &&
result.properties[1] > 0.0 && result.properties[2] == 0.0 &&
result.properties[3] > 0.0 && result.properties[4] > 0.0;
if (!valid_properties) {
add_error(
"abaqus.semantic.invalid_section_data",
"General Beam properties require finite positive A, Iy, "
"Iz, J and Iyz=0.",
data_source(record, 0U));
}
bool valid_orientation = true;
for (std::size_t field = 0; field < result.orientation.size();
++field) {
valid_orientation =
parse_number(
record.data[1][field], result.orientation[field]) &&
valid_orientation;
}
if (!valid_orientation) {
add_error(
"abaqus.semantic.invalid_section_data",
"Beam section orientation components must be finite.",
data_source(record, 1U));
}
result.valid = valid_properties && valid_orientation;
}
if (shear == nullptr) {
return result;
}
const SourceLocation source =
shear->data.empty() ? shear->source : data_source(*shear, 0U);
std::array<double, 3> values{};
bool valid_shear = shear->data.size() == 1U &&
shear->data[0].size() == values.size();
if (valid_shear) {
for (std::size_t field = 0; field < values.size(); ++field) {
valid_shear =
parse_number(shear->data[0][field], values[field]) &&
valid_shear;
}
}
if (!valid_shear || values[0] <= 0.0 || values[1] <= 0.0) {
add_error(
"abaqus.semantic.invalid_transverse_shear_data",
"*TRANSVERSE SHEAR STIFFNESS requires finite positive K23, "
"K13 and numeric SCF=0.",
source);
result.valid = false;
} else if (values[2] != 0.0) {
add_error(
"abaqus.semantic.nonzero_scf",
"Phase 1 supports only SCF=0.",
source);
result.valid = false;
} else {
result.shear_values = values;
}
return result;
}
void validate_inactive_parts(
const std::vector<ResolvedSet>& resolved_sets) {
if (flat_) {
return;
}
for (const ParsedPart& part : deck_.parts) {
if (part.name == part_name_) {
continue;
}
validate_inactive_mesh(part);
RawSetMap element_sets;
for (const ResolvedSet& set : resolved_sets) {
if (set.scope == ResolvedSetScope::part &&
set.scope_name == part.name &&
set.kind == ResolvedSetKind::element) {
element_sets[set.set_name] = set.sorted_unique_labels;
}
}
collect_sections(part.records, element_sets, false);
}
}
void collect_resolved_sets(const std::vector<ResolvedSet>& sets) {
for (const ResolvedSet& set : sets) {
const bool mesh_scope =
@@ -417,9 +557,14 @@ private:
}
}
void collect_sections() {
for (std::size_t index = 0; index < active_records_.size(); ++index) {
const DeckRecord& record = active_records_[index];
void collect_sections(
const std::span<const DeckRecord> records,
const RawSetMap& element_sets,
const bool emit) {
std::set<std::string, std::less<>> assigned_set_names;
std::set<std::int64_t> assigned_elements;
for (std::size_t index = 0; index < records.size(); ++index) {
const DeckRecord& record = records[index];
if (record.keyword == "TRANSVERSE SHEAR STIFFNESS") {
add_error(
"abaqus.semantic.orphan_transverse_shear",
@@ -433,10 +578,10 @@ private:
}
const DeckRecord* shear = nullptr;
if (index + 1U < active_records_.size() &&
active_records_[index + 1U].keyword ==
if (index + 1U < records.size() &&
records[index + 1U].keyword ==
"TRANSVERSE SHEAR STIFFNESS") {
shear = &active_records_[index + 1U];
shear = &records[index + 1U];
++index;
}
@@ -459,7 +604,7 @@ private:
}
if (element_set != nullptr &&
!assigned_element_set_names_.insert(*element_set).second) {
!assigned_set_names.insert(*element_set).second) {
add_error(
"abaqus.semantic.duplicate_section_assignment",
"Element set '" + *element_set +
@@ -481,10 +626,10 @@ private:
}
}
auto resolved_set = active_element_sets_.end();
auto resolved_set = element_sets.end();
if (element_set != nullptr) {
resolved_set = active_element_sets_.find(*element_set);
if (resolved_set == active_element_sets_.end()) {
resolved_set = element_sets.find(*element_set);
if (resolved_set == element_sets.end()) {
add_error(
"abaqus.semantic.missing_element_set",
"Beam section references missing element set '" +
@@ -494,132 +639,48 @@ private:
}
}
std::array<double, 5> properties{};
std::array<double, 3> orientation{};
if (record.data.size() != 2U ||
record.data[0].size() != properties.size() ||
record.data[1].size() != orientation.size()) {
SourceLocation section_data_source = record.source;
if (!record.data.empty()) {
if (record.data[0].size() != properties.size()) {
section_data_source = data_source(record, 0U);
} else if (record.data.size() > 2U) {
section_data_source = data_source(record, 2U);
} else if (record.data.size() == 2U) {
section_data_source = data_source(record, 1U);
}
}
add_error(
"abaqus.semantic.invalid_section_data",
"*BEAM GENERAL SECTION requires exactly A, Iy, Iyz, Iz, "
"J and one three-component orientation row.",
section_data_source);
valid = false;
} else {
bool valid_properties = true;
for (std::size_t field = 0; field < properties.size(); ++field) {
valid_properties = parse_number(
record.data[0][field],
properties[field]) &&
valid_properties;
}
valid_properties =
valid_properties && properties[0] > 0.0 &&
properties[1] > 0.0 && properties[2] == 0.0 &&
properties[3] > 0.0 && properties[4] > 0.0;
if (!valid_properties) {
add_error(
"abaqus.semantic.invalid_section_data",
"General Beam properties require finite positive A, "
"Iy, Iz, J and Iyz=0.",
data_source(record, 0U));
}
bool valid_orientation = true;
for (std::size_t field = 0; field < orientation.size(); ++field) {
valid_orientation = parse_number(
record.data[1][field],
orientation[field]) &&
valid_orientation;
}
if (!valid_orientation) {
add_error(
"abaqus.semantic.invalid_section_data",
"Beam section orientation components must be finite.",
data_source(record, 1U));
}
valid = valid && valid_properties && valid_orientation;
}
SectionData section_data = validate_section_data(record, shear);
valid = valid && section_data.valid;
double shear_area_y = 0.0;
double shear_area_z = 0.0;
ShearPropertySource shear_source =
ShearPropertySource::phase1_default;
if (shear == nullptr) {
shear_area_y = 5.0 * properties[0] / 6.0;
shear_area_y = 5.0 * section_data.properties[0] / 6.0;
shear_area_z = shear_area_y;
} else {
} else if (section_data.shear_values.has_value() &&
material != materials_.end()) {
const SourceLocation shear_source_location =
shear->data.empty() ? shear->source
: data_source(*shear, 0U);
std::array<double, 3> values{};
bool valid_shear =
shear->data.size() == 1U &&
shear->data[0].size() == values.size();
if (valid_shear) {
for (std::size_t field = 0; field < values.size(); ++field) {
valid_shear = parse_number(
shear->data[0][field], values[field]) &&
valid_shear;
}
}
if (!valid_shear || values[0] <= 0.0 || values[1] <= 0.0) {
const double shear_modulus =
material->second.young /
(2.0 * (1.0 + material->second.poisson));
shear_area_y = (*section_data.shear_values)[0] / shear_modulus;
shear_area_z = (*section_data.shear_values)[1] / shear_modulus;
if (!std::isfinite(shear_area_y) ||
!std::isfinite(shear_area_z) || shear_area_y <= 0.0 ||
shear_area_z <= 0.0) {
add_error(
"abaqus.semantic.invalid_transverse_shear_data",
"*TRANSVERSE SHEAR STIFFNESS requires finite positive "
"K23, K13 and numeric SCF=0.",
"Transverse stiffness produces an invalid effective "
"shear area.",
shear_source_location);
valid = false;
} else if (values[2] != 0.0) {
add_error(
"abaqus.semantic.nonzero_scf",
"Phase 1 supports only SCF=0.",
shear_source_location);
valid = false;
} else if (material != materials_.end()) {
const double shear_modulus =
material->second.young /
(2.0 * (1.0 + material->second.poisson));
shear_area_y = values[0] / shear_modulus;
shear_area_z = values[1] / shear_modulus;
if (!std::isfinite(shear_area_y) ||
!std::isfinite(shear_area_z) ||
shear_area_y <= 0.0 || shear_area_z <= 0.0) {
add_error(
"abaqus.semantic.invalid_transverse_shear_data",
"Transverse stiffness produces an invalid effective "
"shear area.",
shear_source_location);
valid = false;
}
shear_source = ShearPropertySource::input;
}
shear_source = ShearPropertySource::input;
}
if (!valid || element_set == nullptr ||
material == materials_.end() ||
resolved_set == active_element_sets_.end()) {
resolved_set == element_sets.end()) {
continue;
}
const SectionId section_id{next_section_id_++};
const SectionAssignment assignment{
material->second.id,
section_id,
};
bool duplicate_member = false;
for (const std::int64_t label : resolved_set->second) {
if (element_assignments_.contains(label)) {
if (assigned_elements.contains(label)) {
duplicate_member = true;
break;
}
@@ -631,6 +692,18 @@ private:
record.source);
continue;
}
for (const std::int64_t label : resolved_set->second) {
assigned_elements.insert(label);
}
if (!emit) {
continue;
}
const SectionId section_id{next_section_id_++};
const SectionAssignment assignment{
material->second.id,
section_id,
};
for (const std::int64_t label : resolved_set->second) {
element_assignments_.emplace(label, assignment);
}
@@ -638,21 +711,28 @@ private:
builder_.add_section({
section_id,
*element_set,
properties[0],
properties[1],
properties[3],
properties[4],
section_data.properties[0],
section_data.properties[1],
section_data.properties[3],
section_data.properties[4],
shear_area_y,
shear_area_z,
shear_source,
Vec3{orientation[0], orientation[1], orientation[2]},
Vec3{
section_data.orientation[0],
section_data.orientation[1],
section_data.orientation[2]},
{},
});
}
}
void collect_elements() {
for (const DeckRecord& record : active_records_) {
void collect_elements(
const std::span<const DeckRecord> records,
const LabelMap& node_ids,
ElementLabelMap& element_ids,
const bool emit) {
for (const DeckRecord& record : records) {
if (record.keyword != "ELEMENT") {
continue;
}
@@ -702,7 +782,7 @@ private:
continue;
}
if (element_ids_.contains(*label)) {
if (element_ids.contains(*label)) {
add_error(
"abaqus.semantic.duplicate_element_label",
"Element label " + std::to_string(*label) +
@@ -711,19 +791,21 @@ private:
continue;
}
const auto assignment = element_assignments_.find(*label);
if (assignment == element_assignments_.end()) {
add_error(
"abaqus.semantic.missing_section",
"B31 element " + std::to_string(*label) +
" has no Beam section assignment.",
record.source);
continue;
auto assignment = element_assignments_.end();
if (emit) {
assignment = element_assignments_.find(*label);
if (assignment == element_assignments_.end()) {
add_error(
"abaqus.semantic.missing_section",
"B31 element " + std::to_string(*label) +
" has no Beam section assignment.",
source);
continue;
}
}
const auto first = node_ids_.find(*first_label);
const auto second = node_ids_.find(*second_label);
if (first == node_ids_.end() || second == node_ids_.end()) {
const auto first = node_ids.find(*first_label);
const auto second = node_ids.find(*second_label);
if (first == node_ids.end() || second == node_ids.end()) {
add_error(
"abaqus.semantic.missing_node",
"B31 element references a missing node label.",
@@ -731,8 +813,14 @@ private:
continue;
}
const ElementId id{next_element_id_++};
element_ids_.emplace(*label, id);
const ElementId id = emit
? ElementId{next_element_id_++}
: ElementId{static_cast<std::int64_t>(
element_ids.size())};
element_ids.emplace(*label, id);
if (!emit) {
continue;
}
builder_.add_beam_element({
id,
EntityOrigin{part_name_, instance_name_, *label},
@@ -1041,7 +1129,6 @@ private:
std::map<std::string, std::vector<std::int64_t>, std::less<>>
active_element_sets_;
std::map<std::int64_t, SectionAssignment> element_assignments_;
std::set<std::string, std::less<>> assigned_element_set_names_;
RawSetMap raw_node_sets_;
RawSetMap raw_assembly_node_sets_;
RawSetMap raw_element_sets_;
+5
View File
@@ -68,3 +68,8 @@ hierarchical_part_target_invalid invalid invalid/hierarchical_part_set_target.in
material_data_invalid invalid invalid/material_data.inp syntax abaqus.syntax.data_without_keyword 2 - - - - - - - - - - -
node_coordinate_invalid invalid invalid/node_invalid_coordinate.inp semantic abaqus.semantic.invalid_number 2 - - - - - - - - - - -
element_missing_node_invalid invalid invalid/element_missing_node.inp semantic abaqus.semantic.missing_node 4 - - - - - - - - - - -
inactive_part_node_invalid invalid invalid/inactive_part_node_surplus.inp semantic abaqus.semantic.invalid_node_data 3 - - - - - - - - - - -
inactive_part_element_invalid invalid invalid/inactive_part_element_type.inp semantic abaqus.semantic.unsupported_element 2 - - - - - - - - - - -
inactive_part_section_invalid invalid invalid/inactive_part_section_data.inp semantic abaqus.semantic.invalid_section_data 3 - - - - - - - - - - -
inactive_part_duplicate_node_invalid invalid invalid/inactive_part_duplicate_node.inp semantic abaqus.semantic.duplicate_node_label 4 - - - - - - - - - - -
inactive_part_missing_node_invalid invalid invalid/inactive_part_missing_node.inp semantic abaqus.semantic.missing_node 5 - - - - - - - - - - -
1 # case_id outcome fixture expected_stage expected_code expected_line node_count element_count node_set_count element_set_count prescribed_dof_count nodal_load_count shear_source shear_area_y shear_area_z checked_node_set checked_element_set
68 material_data_invalid invalid invalid/material_data.inp syntax abaqus.syntax.data_without_keyword 2 - - - - - - - - - - -
69 node_coordinate_invalid invalid invalid/node_invalid_coordinate.inp semantic abaqus.semantic.invalid_number 2 - - - - - - - - - - -
70 element_missing_node_invalid invalid invalid/element_missing_node.inp semantic abaqus.semantic.missing_node 4 - - - - - - - - - - -
71 inactive_part_node_invalid invalid invalid/inactive_part_node_surplus.inp semantic abaqus.semantic.invalid_node_data 3 - - - - - - - - - - -
72 inactive_part_element_invalid invalid invalid/inactive_part_element_type.inp semantic abaqus.semantic.unsupported_element 2 - - - - - - - - - - -
73 inactive_part_section_invalid invalid invalid/inactive_part_section_data.inp semantic abaqus.semantic.invalid_section_data 3 - - - - - - - - - - -
74 inactive_part_duplicate_node_invalid invalid invalid/inactive_part_duplicate_node.inp semantic abaqus.semantic.duplicate_node_label 4 - - - - - - - - - - -
75 inactive_part_missing_node_invalid invalid invalid/inactive_part_missing_node.inp semantic abaqus.semantic.missing_node 5 - - - - - - - - - - -
@@ -0,0 +1,11 @@
*PART, NAME=Unused
*NODE
1, 0.0, 0.0, 0.0
1, 1.0, 0.0, 0.0
*END PART
*PART, NAME=Active
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Active-1, PART=Active
*END INSTANCE
*END ASSEMBLY
@@ -0,0 +1,10 @@
*PART, NAME=Unused
*ELEMENT, TYPE=B32
1, 1, 2
*END PART
*PART, NAME=Active
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Active-1, PART=Active
*END INSTANCE
*END ASSEMBLY
@@ -0,0 +1,12 @@
*PART, NAME=Unused
*NODE
1, 0.0, 0.0, 0.0
*ELEMENT, TYPE=B31
1, 1, 2
*END PART
*PART, NAME=Active
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Active-1, PART=Active
*END INSTANCE
*END ASSEMBLY
@@ -0,0 +1,10 @@
*PART, NAME=Unused
*NODE
1, 0.0, 0.0, 0.0, 9.0
*END PART
*PART, NAME=Active
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Active-1, PART=Active
*END INSTANCE
*END ASSEMBLY
@@ -0,0 +1,11 @@
*PART, NAME=Unused
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
0.0, 1.0, 0.0, 1.0, 1.0
0.0, 1.0, 0.0
*END PART
*PART, NAME=Active
*END PART
*ASSEMBLY, NAME=RootAssembly
*INSTANCE, NAME=Active-1, PART=Active
*END INSTANCE
*END ASSEMBLY
@@ -333,6 +333,92 @@ TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
EXPECT_EQ(result.domain->nodes()[1].origin.part_name, "BeamPart");
}
TEST(ActiveInstance, RejectsInvalidRecordsInUnreferencedParts) {
struct Case final {
std::string_view name;
std::string_view unused_records;
std::string_view code;
std::size_t line;
};
const Case cases[]{
{
"node-surplus",
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n",
"abaqus.semantic.invalid_node_data",
3U,
},
{
"element-type",
"*ELEMENT, TYPE=B32\n1, 1, 2\n",
"abaqus.semantic.unsupported_element",
2U,
},
{
"duplicate-node",
"*NODE\n1, 0.0, 0.0, 0.0\n1, 1.0, 0.0, 0.0\n",
"abaqus.semantic.duplicate_node_label",
4U,
},
{
"missing-node",
"*NODE\n1, 0.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31\n1, 1, 2\n",
"abaqus.semantic.missing_node",
5U,
},
{
"section-data",
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
"MATERIAL=Steel\n"
"0.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n",
"abaqus.semantic.invalid_section_data",
3U,
},
};
const std::string active_model{
"*PART, NAME=BeamPart\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-inactive-part-" + std::string{test_case.name} + ".inp",
"*PART, NAME=Unused\n" + std::string{test_case.unused_records} +
"*END PART\n" + active_model,
};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, test_case.code);
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, test_case.line);
}
}
TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
const TemporaryDeck input{
"fesa-instance-transform.inp",
@@ -315,7 +315,7 @@ TEST(BeamSection, RejectsElementWithoutSectionAssignment) {
const auto result = parse_and_map(input);
expect_diagnostic(result, "abaqus.semantic.missing_section", 4U);
expect_diagnostic(result, "abaqus.semantic.missing_section", 5U);
}
TEST(BeamSection, RejectsDuplicateSectionAssignment) {