feat(abaqus-subset-completion): step 3 — material-section-and-shear-defaults

This commit is contained in:
KOKO\Mimi
2026-08-01 02:59:40 +09:00
parent 8cd2d5b2ae
commit a1fed69c47
5 changed files with 727 additions and 128 deletions
+368 -126
View File
@@ -1,14 +1,19 @@
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <fesa/io/abaqus/active_input.hpp>
#include <fesa/io/abaqus/set_resolver.hpp>
#include <algorithm>
#include <array>
#include <charconv>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <iterator>
#include <map>
#include <optional>
#include <set>
#include <span>
#include <string>
#include <string_view>
#include <utility>
@@ -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<MaterialDefinition> definitions;
std::map<std::string, std::size_t, std::less<>> first_definition;
std::optional<std::size_t> 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<const DeckRecord> 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<ResolvedSet>& 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<std::int64_t>& 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<std::int64_t> 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<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;
}
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<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.",
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<Diagnostic> diagnostics_;
std::span<const DeckRecord> active_records_;
std::span<const DeckRecord> assembly_records_;
bool flat_ = true;
std::string part_name_;
std::string instance_name_;
LabelMap node_ids_;
ElementLabelMap element_ids_;
std::map<std::string, MaterialId, std::less<>> material_ids_;
std::map<std::string, SectionAssignment, std::less<>>
section_assignments_;
std::map<std::string, MaterialProperties, std::less<>> materials_;
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_element_sets_;
std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;
+19
View File
@@ -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 "$<TARGET_FILE:fesa_abaqus_parser_tests>"
--gtest_filter=MaterialMapping.*
)
add_test(
NAME BeamSection
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
--gtest_filter=BeamSection.*
)
add_test(
NAME ShearDefault
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
--gtest_filter=ShearDefault.*
)
add_executable(fesa_deck_to_domain_tests
integration/io/minimal_deck_to_domain_test.cpp
)
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
@@ -0,0 +1,338 @@
#include <fesa/io/abaqus/parser.hpp>
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <algorithm>
#include <filesystem>
#include <fstream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <gtest/gtest.h>
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<std::streamsize>(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