feat(domain-and-input-skeleton): step 3 — active-instance-domain-normalization

This commit is contained in:
KOKO\Mimi
2026-07-30 18:10:52 +09:00
parent 0f53ec48eb
commit 154f8ef4af
5 changed files with 987 additions and 0 deletions
+1
View File
@@ -28,6 +28,7 @@ include(cmake/FesaDependencies.cmake)
add_library(fesa_core STATIC add_library(fesa_core STATIC
src/fesa/core/version.cpp src/fesa/core/version.cpp
src/fesa/io/abaqus/parser.cpp src/fesa/io/abaqus/parser.cpp
src/fesa/io/abaqus/semantic_mapper.cpp
src/fesa/model/domain.cpp src/fesa/model/domain.cpp
src/fesa/model/domain_builder.cpp src/fesa/model/domain_builder.cpp
) )
@@ -0,0 +1,10 @@
#pragma once
#include <fesa/io/abaqus/deck_record.hpp>
#include <fesa/model/domain_builder.hpp>
namespace fesa {
[[nodiscard]] DomainBuildResult map_deck_to_domain(const ParsedDeck& deck);
} // namespace fesa
+684
View File
@@ -0,0 +1,684 @@
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <algorithm>
#include <array>
#include <charconv>
#include <cstddef>
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
namespace fesa {
namespace {
using LabelMap = std::map<std::int64_t, NodeId>;
using ElementLabelMap = std::map<std::int64_t, ElementId>;
using RawSetMap = std::map<std::string, std::vector<std::int64_t>, std::less<>>;
const std::string* parameter(
const DeckRecord& record,
const std::string_view name) {
const auto found = record.parameters.find(name);
return found == record.parameters.end() ? nullptr : &found->second;
}
bool is_mesh_record(const DeckRecord& record) {
return record.keyword == "NODE" || record.keyword == "ELEMENT";
}
class DeckMapper final {
public:
explicit DeckMapper(const ParsedDeck& deck) : deck_{deck} {}
[[nodiscard]] DomainBuildResult map() {
if (!select_active_scope()) {
return failure();
}
collect_materials();
collect_nodes();
collect_raw_sets(*active_records_, false);
if (active_assembly_ != nullptr) {
collect_raw_sets(active_assembly_->records, true);
}
collect_sections();
collect_elements();
emit_sets();
collect_step();
if (!diagnostics_.empty()) {
return failure();
}
return std::move(builder_).build();
}
private:
struct SectionAssignment final {
MaterialId material;
SectionId section;
};
[[nodiscard]] DomainBuildResult failure() {
return {std::nullopt, std::move(diagnostics_)};
}
void add_error(
std::string code,
std::string message,
const SourceLocation& source) {
diagnostics_.push_back({
DiagnosticStage::semantic,
Severity::error,
std::move(code),
std::move(message),
source,
});
}
const std::string* require_parameter(
const DeckRecord& record,
const std::string_view name) {
const std::string* value = parameter(record, name);
if (value == nullptr || value->empty()) {
add_error(
"abaqus.semantic.missing_parameter",
"*" + record.keyword + " requires parameter " +
std::string{name} + ".",
record.source);
return nullptr;
}
return value;
}
std::optional<std::int64_t> parse_label(
const std::string_view text,
const DeckRecord& record,
const std::string_view purpose) {
std::int64_t value = 0;
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
if (parsed.ec != std::errc{} ||
parsed.ptr != text.data() + text.size() || value <= 0) {
add_error(
"abaqus.semantic.invalid_label",
"Invalid " + std::string{purpose} + " label '" +
std::string{text} + "'.",
record.source);
return std::nullopt;
}
return value;
}
std::optional<std::uint8_t> parse_dof(
const std::string_view text,
const DeckRecord& record) {
const std::optional<std::int64_t> value =
parse_label(text, record, "degree-of-freedom");
if (!value.has_value()) {
return std::nullopt;
}
if (*value > 6) {
add_error(
"abaqus.semantic.invalid_dof",
"Phase 1 supports only degrees of freedom 1 through 6.",
record.source);
return std::nullopt;
}
return static_cast<std::uint8_t>(*value);
}
std::optional<double> parse_real(
const std::string_view text,
const DeckRecord& record,
const std::string_view purpose) {
double value = 0.0;
const auto parsed = std::from_chars(
text.data(),
text.data() + text.size(),
value,
std::chars_format::general);
if (parsed.ec != std::errc{} ||
parsed.ptr != text.data() + text.size()) {
add_error(
"abaqus.semantic.invalid_number",
"Invalid " + std::string{purpose} + " value '" +
std::string{text} + "'.",
record.source);
return std::nullopt;
}
return value;
}
bool select_active_scope() {
const bool has_hierarchical_input =
!deck_.parts.empty() || deck_.assembly.has_value();
if (!has_hierarchical_input) {
active_records_ = &deck_.global_records;
return true;
}
if (std::ranges::any_of(deck_.global_records, is_mesh_record)) {
add_error(
"abaqus.semantic.mixed_mesh_organization",
"Flat mesh records cannot be mixed with Part/Assembly input.",
std::ranges::find_if(
deck_.global_records, is_mesh_record)->source);
return false;
}
if (!deck_.assembly.has_value()) {
const SourceLocation source =
deck_.parts.empty() ? SourceLocation{} : deck_.parts[0].source;
add_error(
"abaqus.semantic.assembly_count",
"Hierarchical Phase 1 input requires exactly one Assembly.",
source);
return false;
}
active_assembly_ = &*deck_.assembly;
if (active_assembly_->instances.size() != 1U) {
add_error(
"abaqus.semantic.instance_count",
"Phase 1 requires exactly one Instance.",
active_assembly_->source);
return false;
}
const ParsedInstance& instance = active_assembly_->instances.front();
if (!instance.transform_data.empty()) {
add_error(
"abaqus.semantic.instance_transform",
"Instance translation and rotation data are unsupported.",
instance.source);
return false;
}
const auto part = std::ranges::find(
deck_.parts, instance.part_name, &ParsedPart::name);
if (part == deck_.parts.end()) {
add_error(
"abaqus.semantic.missing_part",
"Instance '" + instance.name + "' references missing Part '" +
instance.part_name + "'.",
instance.source);
return false;
}
active_records_ = &part->records;
part_name_ = part->name;
instance_name_ = instance.name;
return true;
}
void collect_materials() {
const std::string* current_name = nullptr;
for (const DeckRecord& record : deck_.global_records) {
if (record.keyword == "MATERIAL") {
current_name = require_parameter(record, "NAME");
continue;
}
if (record.keyword != "ELASTIC") {
continue;
}
if (current_name == nullptr) {
add_error(
"abaqus.semantic.elastic_without_material",
"*ELASTIC requires a preceding *MATERIAL.",
record.source);
continue;
}
if (record.data.empty() || record.data[0].size() < 2U) {
add_error(
"abaqus.semantic.invalid_elastic_data",
"*ELASTIC requires Young's modulus and Poisson ratio.",
record.source);
continue;
}
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()) {
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});
}
}
void collect_nodes() {
for (const DeckRecord& record : *active_records_) {
if (record.keyword != "NODE") {
continue;
}
for (const auto& row : record.data) {
if (row.size() < 4U) {
add_error(
"abaqus.semantic.invalid_node_data",
"*NODE requires a label and three coordinates.",
record.source);
continue;
}
const auto label = parse_label(row[0], record, "node");
const auto x = parse_real(row[1], record, "node coordinate");
const auto y = parse_real(row[2], record, "node coordinate");
const auto z = parse_real(row[3], record, "node coordinate");
if (!label.has_value() || !x.has_value() || !y.has_value() ||
!z.has_value()) {
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},
});
}
}
}
void collect_raw_sets(
const std::vector<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) {
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;
}
}
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);
}
}
}
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_) {
if (record.keyword != "BEAM GENERAL SECTION") {
continue;
}
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()) {
add_error(
"abaqus.semantic.missing_material",
"Beam section references missing Material '" +
*material_name + "'.",
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;
}
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) {
add_error(
"abaqus.semantic.duplicate_section_assignment",
"Element set '" + *element_set +
"' has more than one section assignment.",
record.source);
continue;
}
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},
{},
});
}
}
void collect_elements() {
for (const DeckRecord& record : *active_records_) {
if (record.keyword != "ELEMENT") {
continue;
}
const std::string* type = require_parameter(record, "TYPE");
const std::string* element_set =
require_parameter(record, "ELSET");
if (type == nullptr || element_set == nullptr) {
continue;
}
if (*type != "B31") {
add_error(
"abaqus.semantic.unsupported_element",
"Phase 1 supports only B31 elements.",
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) {
add_error(
"abaqus.semantic.invalid_element_data",
"B31 element data requires a label and two nodes.",
record.source);
continue;
}
const auto label = parse_label(row[0], record, "element");
const auto first_label =
parse_label(row[1], record, "node");
const auto second_label =
parse_label(row[2], record, "node");
if (!label.has_value() || !first_label.has_value() ||
!second_label.has_value()) {
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()) {
add_error(
"abaqus.semantic.missing_node",
"B31 element references a missing node label.",
record.source);
continue;
}
const ElementId id{next_element_id_++};
element_ids_.emplace(*label, id);
builder_.add_beam_element({
id,
EntityOrigin{part_name_, instance_name_, *label},
{first->second, second->second},
assignment->second.material,
assignment->second.section,
});
}
}
}
template <class Id, class Lookup>
std::vector<Id> resolve_set(
const std::vector<std::int64_t>& labels,
const Lookup& lookup,
const std::string& name) {
std::vector<Id> members;
for (const std::int64_t label : labels) {
const auto found = lookup.find(label);
if (found == lookup.end()) {
diagnostics_.push_back({
DiagnosticStage::semantic,
Severity::error,
"abaqus.semantic.missing_set_member",
"Set '" + name + "' references a missing entity label " +
std::to_string(label) + ".",
std::nullopt,
});
continue;
}
members.push_back(found->second);
}
std::ranges::sort(members);
members.erase(std::ranges::unique(members).begin(), members.end());
return members;
}
void emit_sets() {
for (const auto& [name, labels] : raw_node_sets_) {
std::vector<NodeId> members =
resolve_set<NodeId>(labels, node_ids_, name);
node_sets_[name] = members;
builder_.add_node_set({name, std::move(members)});
}
for (const auto& [name, labels] : raw_element_sets_) {
std::vector<ElementId> members =
resolve_set<ElementId>(labels, element_ids_, name);
builder_.add_element_set({name, std::move(members)});
}
}
std::vector<NodeId> resolve_node_target(
const std::string& target,
const DeckRecord& record) {
std::int64_t label = 0;
const auto parsed =
std::from_chars(target.data(), target.data() + target.size(), label);
if (parsed.ec == std::errc{} &&
parsed.ptr == target.data() + target.size()) {
const auto found = node_ids_.find(label);
if (found != node_ids_.end()) {
return {found->second};
}
} else {
const auto found = node_sets_.find(target);
if (found != node_sets_.end()) {
return found->second;
}
}
add_error(
"abaqus.semantic.missing_node_target",
"Node target '" + target + "' does not resolve.",
record.source);
return {};
}
void collect_boundary(
const DeckRecord& record,
StepDefinition& step) {
for (const auto& row : record.data) {
if (row.size() < 2U) {
add_error(
"abaqus.semantic.invalid_boundary_data",
"*BOUNDARY requires a target and degree of freedom.",
record.source);
continue;
}
const auto first_dof = parse_dof(row[1], record);
const auto last_dof = row.size() >= 3U && !row[2].empty()
? parse_dof(row[2], record)
: first_dof;
const auto value = row.size() >= 4U && !row[3].empty()
? parse_real(
row[3], record, "prescribed value")
: std::optional<double>{0.0};
if (!first_dof.has_value() || !last_dof.has_value() ||
!value.has_value()) {
continue;
}
if (*last_dof < *first_dof) {
add_error(
"abaqus.semantic.invalid_dof_range",
"*BOUNDARY degree-of-freedom range is reversed.",
record.source);
continue;
}
const std::vector<NodeId> nodes =
resolve_node_target(row[0], record);
for (const NodeId node : nodes) {
for (std::uint8_t dof = *first_dof; dof <= *last_dof; ++dof) {
step.prescribed_dofs.push_back({node, dof, *value});
}
}
}
}
void collect_loads(
const DeckRecord& record,
std::map<std::int64_t, std::array<double, 6>>& loads) {
for (const auto& row : record.data) {
if (row.size() < 3U) {
add_error(
"abaqus.semantic.invalid_cload_data",
"*CLOAD requires a target, degree of freedom, and value.",
record.source);
continue;
}
const auto dof = parse_dof(row[1], record);
const auto value = parse_real(row[2], record, "concentrated load");
if (!dof.has_value() || !value.has_value()) {
continue;
}
const std::vector<NodeId> nodes =
resolve_node_target(row[0], record);
for (const NodeId node : nodes) {
loads[node.value()][static_cast<std::size_t>(*dof - 1U)] +=
*value;
}
}
}
void collect_step() {
StepDefinition step;
std::map<std::int64_t, std::array<double, 6>> loads;
bool has_step = false;
for (const DeckRecord& record : deck_.global_records) {
if (record.keyword == "STEP") {
has_step = true;
const std::string* name = parameter(record, "NAME");
step.name = name == nullptr ? "Step-1" : *name;
} else if (record.keyword == "BOUNDARY") {
collect_boundary(record, step);
} else if (record.keyword == "CLOAD") {
collect_loads(record, loads);
}
}
if (!has_step) {
const SourceLocation source =
deck_.global_records.empty()
? SourceLocation{}
: deck_.global_records.front().source;
add_error(
"abaqus.semantic.missing_step",
"Phase 1 input requires one *STEP.",
source);
return;
}
for (const auto& [node, values] : loads) {
step.nodal_loads.push_back({NodeId{node}, values});
}
builder_.set_step(std::move(step));
}
const ParsedDeck& deck_;
DomainBuilder builder_;
std::vector<Diagnostic> diagnostics_;
const std::vector<DeckRecord>* active_records_ = nullptr;
const ParsedAssembly* active_assembly_ = nullptr;
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_;
RawSetMap raw_node_sets_;
RawSetMap raw_element_sets_;
std::map<std::string, std::vector<NodeId>, std::less<>> node_sets_;
std::int64_t next_node_id_ = 0;
std::int64_t next_element_id_ = 0;
std::int64_t next_material_id_ = 0;
std::int64_t next_section_id_ = 0;
};
} // namespace
DomainBuildResult map_deck_to_domain(const ParsedDeck& deck) {
return DeckMapper{deck}.map();
}
} // namespace fesa
+30
View File
@@ -140,3 +140,33 @@ add_test(
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>" COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
--gtest_filter=ScopedDeck.* --gtest_filter=ScopedDeck.*
) )
add_executable(fesa_deck_to_domain_tests
integration/io/minimal_deck_to_domain_test.cpp
)
target_compile_features(fesa_deck_to_domain_tests PRIVATE cxx_std_20)
target_compile_options(fesa_deck_to_domain_tests PRIVATE /W4 /permissive- /EHsc)
target_compile_definitions(
fesa_deck_to_domain_tests
PRIVATE
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
)
target_link_libraries(fesa_deck_to_domain_tests
PRIVATE
fesa_core
GTest::gtest_main
)
add_test(
NAME DeckToDomain
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=DeckToDomain.*
)
add_test(
NAME ActiveInstance
COMMAND "$<TARGET_FILE:fesa_deck_to_domain_tests>"
--gtest_filter=ActiveInstance.*
)
@@ -0,0 +1,262 @@
#include <fesa/io/abaqus/parser.hpp>
#include <fesa/io/abaqus/semantic_mapper.hpp>
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string_view>
#include <system_error>
#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_;
};
std::filesystem::path fixture_path(std::string_view name) {
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
"abaqus" / name;
}
fesa::DomainBuildResult parse_and_map(const std::filesystem::path& path) {
const auto parsed = fesa::parse_deck(path);
if (!parsed.deck.has_value()) {
return {std::nullopt, parsed.diagnostics};
}
return fesa::map_deck_to_domain(*parsed.deck);
}
bool has_diagnostic(
const fesa::DomainBuildResult& result,
const std::string_view code) {
return std::ranges::any_of(
result.diagnostics,
[code](const fesa::Diagnostic& diagnostic) {
return diagnostic.code == code;
});
}
void expect_equivalent_analysis_data(
const fesa::Domain& flat,
const fesa::Domain& hierarchical) {
ASSERT_EQ(flat.nodes().size(), hierarchical.nodes().size());
ASSERT_EQ(flat.beam_elements().size(), hierarchical.beam_elements().size());
ASSERT_EQ(flat.materials().size(), hierarchical.materials().size());
ASSERT_EQ(flat.sections().size(), hierarchical.sections().size());
for (std::size_t index = 0; index < flat.nodes().size(); ++index) {
const auto& flat_node = flat.nodes()[index];
const auto& hierarchical_node = hierarchical.nodes()[index];
EXPECT_EQ(flat_node.id, hierarchical_node.id);
EXPECT_DOUBLE_EQ(flat_node.position.x, hierarchical_node.position.x);
EXPECT_DOUBLE_EQ(flat_node.position.y, hierarchical_node.position.y);
EXPECT_DOUBLE_EQ(flat_node.position.z, hierarchical_node.position.z);
}
const auto& flat_element = flat.beam_elements().front();
const auto& hierarchical_element = hierarchical.beam_elements().front();
EXPECT_EQ(flat_element.id, hierarchical_element.id);
EXPECT_EQ(flat_element.nodes, hierarchical_element.nodes);
EXPECT_EQ(flat_element.material, hierarchical_element.material);
EXPECT_EQ(flat_element.section, hierarchical_element.section);
const auto& flat_material = flat.materials().front();
const auto& hierarchical_material = hierarchical.materials().front();
EXPECT_EQ(flat_material.id, hierarchical_material.id);
EXPECT_EQ(flat_material.name, hierarchical_material.name);
EXPECT_DOUBLE_EQ(flat_material.young, hierarchical_material.young);
EXPECT_DOUBLE_EQ(flat_material.poisson, hierarchical_material.poisson);
const auto& flat_section = flat.sections().front();
const auto& hierarchical_section = hierarchical.sections().front();
EXPECT_EQ(flat_section.id, hierarchical_section.id);
EXPECT_EQ(flat_section.name, hierarchical_section.name);
EXPECT_DOUBLE_EQ(flat_section.area, hierarchical_section.area);
EXPECT_DOUBLE_EQ(flat_section.iy, hierarchical_section.iy);
EXPECT_DOUBLE_EQ(flat_section.iz, hierarchical_section.iz);
EXPECT_DOUBLE_EQ(flat_section.torsion_j, hierarchical_section.torsion_j);
EXPECT_DOUBLE_EQ(
flat_section.shear_area_y, hierarchical_section.shear_area_y);
EXPECT_DOUBLE_EQ(
flat_section.shear_area_z, hierarchical_section.shear_area_z);
ASSERT_EQ(
flat.step().prescribed_dofs.size(),
hierarchical.step().prescribed_dofs.size());
for (std::size_t index = 0;
index < flat.step().prescribed_dofs.size();
++index) {
const auto& flat_value = flat.step().prescribed_dofs[index];
const auto& hierarchical_value =
hierarchical.step().prescribed_dofs[index];
EXPECT_EQ(flat_value.node, hierarchical_value.node);
EXPECT_EQ(flat_value.dof, hierarchical_value.dof);
EXPECT_DOUBLE_EQ(flat_value.value, hierarchical_value.value);
}
ASSERT_EQ(flat.step().nodal_loads.size(), 1U);
ASSERT_EQ(hierarchical.step().nodal_loads.size(), 1U);
EXPECT_EQ(
flat.step().nodal_loads[0].node,
hierarchical.step().nodal_loads[0].node);
EXPECT_EQ(
flat.step().nodal_loads[0].values,
hierarchical.step().nodal_loads[0].values);
}
TEST(DeckToDomain, NormalizesFlatAndSingleInstanceDecksEquivalently) {
const auto flat =
parse_and_map(fixture_path("minimal_cantilever.inp"));
const auto hierarchical =
parse_and_map(fixture_path("minimal_part_instance_cantilever.inp"));
ASSERT_TRUE(flat.domain.has_value());
ASSERT_TRUE(hierarchical.domain.has_value());
EXPECT_TRUE(flat.diagnostics.empty());
EXPECT_TRUE(hierarchical.diagnostics.empty());
expect_equivalent_analysis_data(*flat.domain, *hierarchical.domain);
for (const auto& node : flat.domain->nodes()) {
EXPECT_TRUE(node.origin.part_name.empty());
EXPECT_TRUE(node.origin.instance_name.empty());
}
for (const auto& node : hierarchical.domain->nodes()) {
EXPECT_EQ(node.origin.part_name, "BeamPart");
EXPECT_EQ(node.origin.instance_name, "Beam-1");
}
EXPECT_EQ(
hierarchical.domain->beam_elements()[0].origin,
(fesa::EntityOrigin{"BeamPart", "Beam-1", 1}));
const auto& section = hierarchical.domain->sections().front();
EXPECT_DOUBLE_EQ(section.shear_area_y, 5.0 * section.area / 6.0);
EXPECT_DOUBLE_EQ(section.shear_area_z, 5.0 * section.area / 6.0);
EXPECT_EQ(
section.shear_source,
fesa::ShearPropertySource::phase1_default);
}
TEST(ActiveInstance, ExcludesPartsNotReferencedByTheInstance) {
const TemporaryDeck input{
"fesa-active-instance.inp",
"*PART, NAME=Unused\n"
"*NODE\n"
"99, 9.0, 0.0, 0.0\n"
"*END PART\n"
"*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, NAME=Load\n"
"*STATIC\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
ASSERT_EQ(result.domain->nodes().size(), 2U);
EXPECT_EQ(result.domain->nodes()[0].origin.part_name, "BeamPart");
EXPECT_EQ(result.domain->nodes()[1].origin.part_name, "BeamPart");
}
TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
const TemporaryDeck input{
"fesa-instance-transform.inp",
"*PART, NAME=BeamPart\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"1.0, 2.0, 3.0\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_transform"));
ASSERT_FALSE(result.diagnostics.empty());
ASSERT_TRUE(result.diagnostics.front().source.has_value());
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
}
TEST(ActiveInstance, RejectsMissingPartReferenceWithSourceDiagnostic) {
const TemporaryDeck input{
"fesa-missing-part.inp",
"*PART, NAME=OtherPart\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=MissingPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
ASSERT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_part"));
ASSERT_FALSE(result.diagnostics.empty());
ASSERT_TRUE(result.diagnostics.front().source.has_value());
EXPECT_EQ(result.diagnostics.front().source->line, 4U);
}
TEST(ActiveInstance, RejectsMultipleInstances) {
const TemporaryDeck input{
"fesa-multiple-instances.inp",
"*PART, NAME=BeamPart\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*INSTANCE, NAME=Beam-2, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, "abaqus.semantic.instance_count"));
}
} // namespace