feat(domain-and-input-skeleton): step 3 — active-instance-domain-normalization
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user