From a1fed69c4705cfb3c96dc6a4d1903e98b042add4 Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Sat, 1 Aug 2026 02:59:40 +0900 Subject: [PATCH] =?UTF-8?q?feat(abaqus-subset-completion):=20step=203=20?= =?UTF-8?q?=E2=80=94=20material-section-and-shear-defaults?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/fesa/io/abaqus/semantic_mapper.cpp | 494 +++++++++++++----- tests/CMakeLists.txt | 19 + tests/fixtures/abaqus/minimal_cantilever.inp | 2 +- .../minimal_part_instance_cantilever.inp | 2 +- .../abaqus/material_section_mapping_test.cpp | 338 ++++++++++++ 5 files changed, 727 insertions(+), 128 deletions(-) create mode 100644 tests/unit/io/abaqus/material_section_mapping_test.cpp diff --git a/src/fesa/io/abaqus/semantic_mapper.cpp b/src/fesa/io/abaqus/semantic_mapper.cpp index 9dc792f..795a9b1 100644 --- a/src/fesa/io/abaqus/semantic_mapper.cpp +++ b/src/fesa/io/abaqus/semantic_mapper.cpp @@ -1,14 +1,19 @@ #include #include +#include #include #include #include +#include #include #include +#include #include #include +#include +#include #include #include #include @@ -28,6 +33,24 @@ const std::string* parameter( return found == record.parameters.end() ? nullptr : &found->second; } +SourceLocation data_source( + const DeckRecord& record, + const std::size_t row) { + return row < record.data_sources.size() + ? record.data_sources[row] + : record.source; +} + +bool parse_number(const std::string_view text, double& value) { + const auto parsed = std::from_chars( + text.data(), + text.data() + text.size(), + value, + std::chars_format::general); + return parsed.ec == std::errc{} && + parsed.ptr == text.data() + text.size() && std::isfinite(value); +} + class DeckMapper final { public: explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {} @@ -39,15 +62,20 @@ public: return failure(); } active_records_ = selected.input->part_records; - assembly_records_ = selected.input->assembly_records; + flat_ = selected.input->flat; part_name_ = std::move(selected.input->part_name); instance_name_ = std::move(selected.input->instance_name); collect_materials(); collect_nodes(); - collect_raw_sets(active_records_, false); - if (!assembly_records_.empty()) { - collect_raw_sets(assembly_records_, 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 { + collect_resolved_sets(resolved_sets.sets); } collect_sections(); collect_elements(); @@ -66,6 +94,19 @@ private: SectionId section; }; + struct MaterialProperties final { + MaterialId id; + double young; + double poisson; + }; + + struct MaterialDefinition final { + std::string name; + SourceLocation source; + const DeckRecord* elastic = nullptr; + bool duplicate = false; + }; + [[nodiscard]] DomainBuildResult failure() { return {std::nullopt, std::move(diagnostics_)}; } @@ -158,47 +199,105 @@ private: } void collect_materials() { - const std::string* current_name = nullptr; + std::vector definitions; + std::map> first_definition; + std::optional current_definition; + for (const DeckRecord& record : deck_.global_records) { if (record.keyword == "MATERIAL") { - current_name = require_parameter(record, "NAME"); + const std::string* name = require_parameter(record, "NAME"); + if (name == nullptr) { + current_definition.reset(); + continue; + } + + const bool duplicate = first_definition.contains(*name); + if (duplicate) { + add_error( + "abaqus.semantic.duplicate_material", + "Material '" + *name + + "' is defined more than once.", + record.source); + } + definitions.push_back({ + *name, + record.source, + nullptr, + duplicate, + }); + current_definition = definitions.size() - 1U; + first_definition.try_emplace(*name, *current_definition); continue; } if (record.keyword != "ELASTIC") { continue; } - if (current_name == nullptr) { + if (!current_definition.has_value()) { add_error( "abaqus.semantic.elastic_without_material", "*ELASTIC requires a preceding *MATERIAL.", record.source); continue; } - if (record.data.empty() || record.data[0].size() < 2U) { + + MaterialDefinition& definition = + definitions[*current_definition]; + if (definition.elastic != nullptr) { add_error( "abaqus.semantic.invalid_elastic_data", - "*ELASTIC requires Young's modulus and Poisson ratio.", + "A material accepts exactly one *ELASTIC record.", record.source); continue; } + definition.elastic = &record; + } - const auto young = - parse_real(record.data[0][0], record, "Young's modulus"); - const auto poisson = - parse_real(record.data[0][1], record, "Poisson ratio"); - if (!young.has_value() || !poisson.has_value()) { + for (const MaterialDefinition& definition : definitions) { + if (definition.duplicate) { + continue; + } + if (definition.elastic == nullptr) { + add_error( + "abaqus.semantic.missing_elastic", + "Material '" + definition.name + + "' requires one *ELASTIC record.", + definition.source); + continue; + } + + const DeckRecord& elastic = *definition.elastic; + const SourceLocation source = + elastic.data.empty() ? elastic.source + : data_source(elastic, 0U); + if (elastic.data.size() != 1U || + elastic.data[0].size() != 2U || + elastic.data[0][0].empty() || + elastic.data[0][1].empty()) { + add_error( + "abaqus.semantic.invalid_elastic_data", + "*ELASTIC requires exactly one E, nu data row.", + source); + continue; + } + + double young = 0.0; + double poisson = 0.0; + if (!parse_number(elastic.data[0][0], young) || + !parse_number(elastic.data[0][1], poisson) || + young <= 0.0 || poisson <= -1.0 || poisson >= 0.5) { + add_error( + "abaqus.semantic.invalid_elastic_data", + "*ELASTIC requires finite E > 0 and -1 < nu < 0.5.", + source); continue; } const MaterialId id{next_material_id_++}; - if (!material_ids_.emplace(*current_name, id).second) { - add_error( - "abaqus.semantic.duplicate_material", - "Material '" + *current_name + "' is defined more than once.", - record.source); - continue; - } - builder_.add_material({id, *current_name, *young, *poisson}); + materials_.emplace( + definition.name, + MaterialProperties{id, young, poisson}); + builder_.add_material( + {id, definition.name, young, poisson}); } } @@ -236,125 +335,267 @@ private: } } - void collect_raw_sets( - const std::span records, - const bool assembly_scope) { - for (const DeckRecord& record : records) { - const bool is_node_set = record.keyword == "NSET"; - const bool is_element_set = record.keyword == "ELSET"; - if (!is_node_set && !is_element_set) { + void collect_resolved_sets(const std::vector& sets) { + for (const ResolvedSet& set : sets) { + const bool mesh_scope = + (flat_ && set.scope == ResolvedSetScope::global) || + (!flat_ && set.scope == ResolvedSetScope::part && + set.scope_name == part_name_); + const bool assembly_scope = + !flat_ && set.scope == ResolvedSetScope::assembly && + deck_.assembly.has_value() && + set.scope_name == deck_.assembly->name; + if (!mesh_scope && !assembly_scope) { continue; } - const std::string* name = - require_parameter(record, is_node_set ? "NSET" : "ELSET"); - if (name == nullptr) { - continue; - } - if (assembly_scope) { - const std::string* instance = parameter(record, "INSTANCE"); - if (instance == nullptr || *instance != instance_name_) { - add_error( - "abaqus.semantic.wrong_instance", - "Assembly set '" + *name + - "' must reference the active Instance.", - record.source); - continue; - } - } + RawSetMap& target = set.kind == ResolvedSetKind::node + ? raw_node_sets_ + : raw_element_sets_; + std::vector& labels = target[set.set_name]; + labels.insert( + labels.end(), + set.sorted_unique_labels.begin(), + set.sorted_unique_labels.end()); + std::ranges::sort(labels); + labels.erase(std::ranges::unique(labels).begin(), labels.end()); - std::vector labels; - for (const auto& row : record.data) { - for (const std::string& field : row) { - if (field.empty()) { - continue; - } - const auto label = - parse_label(field, record, "set member"); - if (label.has_value()) { - labels.push_back(*label); - } - } + if (mesh_scope && set.kind == ResolvedSetKind::element) { + active_element_sets_[set.set_name] = + set.sorted_unique_labels; } - RawSetMap& sets = is_node_set ? raw_node_sets_ : raw_element_sets_; - sets[*name] = std::move(labels); } } void collect_sections() { - for (const DeckRecord& record : active_records_) { + for (std::size_t index = 0; index < active_records_.size(); ++index) { + const DeckRecord& record = active_records_[index]; + if (record.keyword == "TRANSVERSE SHEAR STIFFNESS") { + add_error( + "abaqus.semantic.orphan_transverse_shear", + "*TRANSVERSE SHEAR STIFFNESS must immediately follow " + "*BEAM GENERAL SECTION.", + record.source); + continue; + } if (record.keyword != "BEAM GENERAL SECTION") { continue; } + const DeckRecord* shear = nullptr; + if (index + 1U < active_records_.size() && + active_records_[index + 1U].keyword == + "TRANSVERSE SHEAR STIFFNESS") { + shear = &active_records_[index + 1U]; + ++index; + } + + bool valid = true; + const std::string* section_type = + require_parameter(record, "SECTION"); const std::string* element_set = require_parameter(record, "ELSET"); const std::string* material_name = require_parameter(record, "MATERIAL"); - if (element_set == nullptr || material_name == nullptr) { - continue; - } - const auto material = material_ids_.find(*material_name); - if (material == material_ids_.end()) { + if (section_type == nullptr || element_set == nullptr || + material_name == nullptr) { + valid = false; + } else if (*section_type != "GENERAL") { add_error( - "abaqus.semantic.missing_material", - "Beam section references missing Material '" + - *material_name + "'.", + "abaqus.semantic.unsupported_section", + "Phase 1 supports only SECTION=GENERAL.", record.source); - continue; - } - if (record.data.size() < 2U || - record.data[0].size() < 5U || - record.data[1].size() < 3U) { - add_error( - "abaqus.semantic.invalid_section_data", - "*BEAM GENERAL SECTION requires general properties and " - "an orientation vector.", - record.source); - continue; + valid = false; } - const auto area = parse_real(record.data[0][0], record, "area"); - const auto iy = parse_real(record.data[0][1], record, "Iy"); - const auto iz = parse_real(record.data[0][3], record, "Iz"); - const auto torsion = - parse_real(record.data[0][4], record, "torsion J"); - const auto ox = - parse_real(record.data[1][0], record, "orientation"); - const auto oy = - parse_real(record.data[1][1], record, "orientation"); - const auto oz = - parse_real(record.data[1][2], record, "orientation"); - if (!area.has_value() || !iy.has_value() || !iz.has_value() || - !torsion.has_value() || !ox.has_value() || !oy.has_value() || - !oz.has_value()) { - continue; - } - - const SectionId section_id{next_section_id_++}; - if (!section_assignments_ - .emplace( - *element_set, - SectionAssignment{material->second, section_id}) - .second) { + if (element_set != nullptr && + !assigned_element_set_names_.insert(*element_set).second) { add_error( "abaqus.semantic.duplicate_section_assignment", "Element set '" + *element_set + "' has more than one section assignment.", record.source); + valid = false; + } + + auto material = materials_.end(); + if (material_name != nullptr) { + material = materials_.find(*material_name); + if (material == materials_.end()) { + add_error( + "abaqus.semantic.missing_material", + "Beam section references missing Material '" + + *material_name + "'.", + record.source); + valid = false; + } + } + + auto resolved_set = active_element_sets_.end(); + if (element_set != nullptr) { + resolved_set = active_element_sets_.find(*element_set); + if (resolved_set == active_element_sets_.end()) { + add_error( + "abaqus.semantic.missing_element_set", + "Beam section references missing element set '" + + *element_set + "'.", + record.source); + valid = false; + } + } + + std::array properties{}; + std::array 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; + } + + 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_z = shear_area_y; + } else { + const SourceLocation shear_source_location = + shear->data.empty() ? shear->source + : data_source(*shear, 0U); + std::array 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.", + 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; + } + } + + if (!valid || element_set == nullptr || + material == materials_.end() || + resolved_set == active_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)) { + duplicate_member = true; + break; + } + } + if (duplicate_member) { + add_error( + "abaqus.semantic.duplicate_section_assignment", + "An element has more than one section assignment.", + record.source); + continue; + } + for (const std::int64_t label : resolved_set->second) { + element_assignments_.emplace(label, assignment); + } + builder_.add_section({ section_id, *element_set, - *area, - *iy, - *iz, - *torsion, - 5.0 * *area / 6.0, - 5.0 * *area / 6.0, - ShearPropertySource::phase1_default, - Vec3{*ox, *oy, *oz}, + properties[0], + properties[1], + properties[3], + properties[4], + shear_area_y, + shear_area_z, + shear_source, + Vec3{orientation[0], orientation[1], orientation[2]}, {}, }); } @@ -367,9 +608,7 @@ private: } const std::string* type = require_parameter(record, "TYPE"); - const std::string* element_set = - require_parameter(record, "ELSET"); - if (type == nullptr || element_set == nullptr) { + if (type == nullptr) { continue; } if (*type != "B31") { @@ -379,15 +618,6 @@ private: record.source); continue; } - const auto assignment = section_assignments_.find(*element_set); - if (assignment == section_assignments_.end()) { - add_error( - "abaqus.semantic.missing_section", - "Element set '" + *element_set + - "' has no Beam section assignment.", - record.source); - continue; - } for (const auto& row : record.data) { if (row.size() < 3U) { @@ -407,6 +637,16 @@ 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; + } + 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()) { @@ -601,14 +841,16 @@ private: DomainBuilder builder_; std::vector diagnostics_; std::span active_records_; - std::span assembly_records_; + bool flat_ = true; std::string part_name_; std::string instance_name_; LabelMap node_ids_; ElementLabelMap element_ids_; - std::map> material_ids_; - std::map> - section_assignments_; + std::map> materials_; + std::map, std::less<>> + active_element_sets_; + std::map element_assignments_; + std::set> assigned_element_set_names_; RawSetMap raw_node_sets_; RawSetMap raw_element_sets_; std::map, std::less<>> node_sets_; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 113ed03..51e80f1 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -120,6 +120,7 @@ add_test( add_executable(fesa_abaqus_parser_tests unit/io/abaqus/active_input_test.cpp unit/io/abaqus/input_contract_test.cpp + unit/io/abaqus/material_section_mapping_test.cpp unit/io/abaqus/parser_test.cpp unit/io/abaqus/set_resolution_test.cpp ) @@ -188,6 +189,24 @@ add_test( --gtest_filter=SemanticScope.* ) +add_test( + NAME MaterialMapping + COMMAND "$" + --gtest_filter=MaterialMapping.* +) + +add_test( + NAME BeamSection + COMMAND "$" + --gtest_filter=BeamSection.* +) + +add_test( + NAME ShearDefault + COMMAND "$" + --gtest_filter=ShearDefault.* +) + add_executable(fesa_deck_to_domain_tests integration/io/minimal_deck_to_domain_test.cpp ) diff --git a/tests/fixtures/abaqus/minimal_cantilever.inp b/tests/fixtures/abaqus/minimal_cantilever.inp index 39f876c..bee41c4 100644 --- a/tests/fixtures/abaqus/minimal_cantilever.inp +++ b/tests/fixtures/abaqus/minimal_cantilever.inp @@ -15,7 +15,7 @@ *ELASTIC 210000.0, 0.3 *BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel -1.0, 1.0, 1.0, 1.0, 1.0 +1.0, 1.0, 0.0, 1.0, 1.0 0.0, 1.0, 0.0 *BOUNDARY Fixed, 1, 6 diff --git a/tests/fixtures/abaqus/minimal_part_instance_cantilever.inp b/tests/fixtures/abaqus/minimal_part_instance_cantilever.inp index 17c4a2c..7fe1bd5 100644 --- a/tests/fixtures/abaqus/minimal_part_instance_cantilever.inp +++ b/tests/fixtures/abaqus/minimal_part_instance_cantilever.inp @@ -12,7 +12,7 @@ *ELSET, ELSET=Beam 1 *BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel -1.0, 1.0, 1.0, 1.0, 1.0 +1.0, 1.0, 0.0, 1.0, 1.0 0.0, 1.0, 0.0 *END PART diff --git a/tests/unit/io/abaqus/material_section_mapping_test.cpp b/tests/unit/io/abaqus/material_section_mapping_test.cpp new file mode 100644 index 0000000..821df6a --- /dev/null +++ b/tests/unit/io/abaqus/material_section_mapping_test.cpp @@ -0,0 +1,338 @@ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace { + +class TemporaryDeck final { +public: + TemporaryDeck(std::string_view name, std::string_view contents) + : path_{std::filesystem::path{testing::TempDir()} / name} { + std::ofstream output{path_, std::ios::binary}; + output.write( + contents.data(), static_cast(contents.size())); + if (!output) { + throw std::runtime_error{"Failed to write temporary Abaqus deck."}; + } + } + + ~TemporaryDeck() { + std::error_code error; + std::filesystem::remove(path_, error); + } + + TemporaryDeck(const TemporaryDeck&) = delete; + TemporaryDeck& operator=(const TemporaryDeck&) = delete; + + [[nodiscard]] const std::filesystem::path& path() const noexcept { + return path_; + } + +private: + std::filesystem::path path_; +}; + +fesa::DomainBuildResult parse_and_map(const TemporaryDeck& input) { + auto parsed = fesa::parse_deck(input.path()); + if (!parsed.deck.has_value()) { + return {std::nullopt, std::move(parsed.diagnostics)}; + } + return fesa::map_deck_to_domain(*parsed.deck); +} + +void expect_diagnostic( + const fesa::DomainBuildResult& result, + const std::string_view code, + const std::size_t line) { + EXPECT_FALSE(result.domain.has_value()); + const auto diagnostic = std::ranges::find_if( + result.diagnostics, + [code, line](const fesa::Diagnostic& candidate) { + return candidate.stage == fesa::DiagnosticStage::semantic && + candidate.code == code && candidate.source.has_value() && + candidate.source->line == line; + }); + EXPECT_NE(diagnostic, result.diagnostics.end()); +} + +std::string_view valid_flat_prefix() { + return + "*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" + "*MATERIAL, NAME=Steel\n" + "*ELASTIC\n" + "200.0, 0.25\n"; +} + +std::string complete_flat_deck(std::string_view section_and_shear) { + std::string contents{valid_flat_prefix()}; + contents.append(section_and_shear); + contents.append( + "*STEP\n" + "*STATIC\n" + "*END STEP\n"); + return contents; +} + +TEST(MaterialMapping, ResolvesForwardMaterialAndExplicitElsetAssignment) { + const TemporaryDeck input{ + "fesa-forward-material.inp", + "*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\n" + "1, 1, 2\n" + "*ELSET, ELSET=Beam\n" + "1\n" + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n" + "*END PART\n" + "*ASSEMBLY, NAME=Assembly\n" + "*INSTANCE, NAME=Beam-1, PART=BeamPart\n" + "*END INSTANCE\n" + "*END ASSEMBLY\n" + "*MATERIAL, NAME=Steel\n" + "*ELASTIC\n" + "200.0, 0.25\n" + "*STEP\n" + "*STATIC\n" + "*END STEP\n"}; + + const auto result = parse_and_map(input); + + ASSERT_TRUE(result.domain.has_value()); + ASSERT_TRUE(result.diagnostics.empty()); + ASSERT_EQ(result.domain->materials().size(), 1U); + EXPECT_DOUBLE_EQ(result.domain->materials()[0].young, 200.0); + EXPECT_DOUBLE_EQ(result.domain->materials()[0].poisson, 0.25); + ASSERT_EQ(result.domain->sections().size(), 1U); + EXPECT_EQ( + result.domain->beam_elements()[0].material, + result.domain->materials()[0].id); + EXPECT_EQ( + result.domain->beam_elements()[0].section, + result.domain->sections()[0].id); +} + +TEST(ShearDefault, PreservesPhase1DefaultEffectiveAreasAndSource) { + const TemporaryDeck input{ + "fesa-default-shear.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + ASSERT_TRUE(result.domain.has_value()); + ASSERT_EQ(result.domain->sections().size(), 1U); + const fesa::BeamSection& section = result.domain->sections()[0]; + EXPECT_DOUBLE_EQ(section.shear_area_y, 1.0); + EXPECT_DOUBLE_EQ(section.shear_area_z, 1.0); + EXPECT_EQ( + section.shear_source, + fesa::ShearPropertySource::phase1_default); +} + +TEST(ShearDefault, ConvertsExplicitK23AndK13UsingIsotropicShearModulus) { + const TemporaryDeck input{ + "fesa-explicit-shear.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n" + "*TRANSVERSE SHEAR STIFFNESS\n" + "64.0, 40.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + ASSERT_TRUE(result.domain.has_value()); + ASSERT_EQ(result.domain->sections().size(), 1U); + const fesa::BeamSection& section = result.domain->sections()[0]; + EXPECT_DOUBLE_EQ(section.shear_area_y, 0.8); + EXPECT_DOUBLE_EQ(section.shear_area_z, 0.5); + EXPECT_EQ(section.shear_source, fesa::ShearPropertySource::input); +} + +TEST(BeamSection, RejectsInvalidElasticPropertiesAtTheDataRow) { + const TemporaryDeck input{ + "fesa-invalid-elastic.inp", + "*MATERIAL, NAME=Steel\n" + "*ELASTIC\n" + "-1.0, 0.25\n" + "*STEP\n" + "*STATIC\n" + "*END STEP\n"}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.invalid_elastic_data", 3U); +} + +TEST(BeamSection, RejectsMaterialWithoutElasticData) { + const TemporaryDeck input{ + "fesa-missing-elastic.inp", + "*MATERIAL, NAME=Steel\n" + "*STEP\n" + "*STATIC\n" + "*END STEP\n"}; + + const auto result = parse_and_map(input); + + expect_diagnostic(result, "abaqus.semantic.missing_elastic", 1U); +} + +TEST(MaterialMapping, RejectsDuplicateMaterialDefinition) { + const TemporaryDeck input{ + "fesa-duplicate-material.inp", + "*MATERIAL, NAME=Steel\n" + "*ELASTIC\n" + "200.0, 0.25\n" + "*MATERIAL, NAME=Steel\n" + "*ELASTIC\n" + "210.0, 0.3\n" + "*STEP\n" + "*STATIC\n" + "*END STEP\n"}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.duplicate_material", 4U); +} + +TEST(MaterialMapping, RejectsSectionWithMissingMaterialReference) { + const TemporaryDeck input{ + "fesa-missing-material.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Missing\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic(result, "abaqus.semantic.missing_material", 11U); +} + +TEST(BeamSection, RejectsNonzeroProductMomentAtTheDataRow) { + const TemporaryDeck input{ + "fesa-nonzero-iyz.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.1, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.invalid_section_data", 12U); +} + +TEST(BeamSection, RejectsNonpositiveGeneralPropertyAtTheDataRow) { + const TemporaryDeck input{ + "fesa-invalid-area.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "0.0, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.invalid_section_data", 12U); +} + +TEST(BeamSection, RejectsNonfiniteOrientationAtTheOrientationRow) { + const TemporaryDeck input{ + "fesa-nonfinite-orientation.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, nan, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.invalid_section_data", 13U); +} + +TEST(BeamSection, RejectsSectionWhoseElementSetDoesNotExist) { + const TemporaryDeck input{ + "fesa-missing-section-elset.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Missing, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.missing_element_set", 11U); +} + +TEST(BeamSection, RejectsNonzeroScfAtTheDataRow) { + const TemporaryDeck input{ + "fesa-nonzero-scf.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n" + "*TRANSVERSE SHEAR STIFFNESS\n" + "64.0, 40.0, 0.25\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic(result, "abaqus.semantic.nonzero_scf", 15U); +} + +TEST(BeamSection, RejectsElementWithoutSectionAssignment) { + const TemporaryDeck input{ + "fesa-missing-section.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Other, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n" + "*ELSET, ELSET=Other\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic(result, "abaqus.semantic.missing_section", 4U); +} + +TEST(BeamSection, RejectsDuplicateSectionAssignment) { + const TemporaryDeck input{ + "fesa-duplicate-section.inp", + complete_flat_deck( + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n" + "*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n" + "1.2, 2.0, 0.0, 3.0, 4.0\n" + "0.0, 1.0, 0.0\n")}; + + const auto result = parse_and_map(input); + + expect_diagnostic( + result, "abaqus.semantic.duplicate_section_assignment", 14U); +} + +} // namespace