Files
FESA/tests/integration/io/minimal_deck_to_domain_test.cpp
T

730 lines
24 KiB
C++

#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>
#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;
});
}
const fesa::Diagnostic* find_diagnostic(
const fesa::DomainBuildResult& result,
const std::string_view code) {
const auto found = std::ranges::find(
result.diagnostics, code, &fesa::Diagnostic::code);
return found == result.diagnostics.end() ? nullptr : &*found;
}
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(DeckToDomain, RequiresExactNodeAndB31DataWidthsAndNonemptyRecords) {
struct Case final {
std::string_view name;
std::string_view mesh;
std::string_view code;
};
const Case cases[]{
{
"node-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"node-empty",
"*NODE\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2\n",
"abaqus.semantic.invalid_node_data",
},
{
"element-surplus-field",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n1, 1, 2, 3\n",
"abaqus.semantic.invalid_element_data",
},
{
"element-empty",
"*NODE\n1, 0.0, 0.0, 0.0\n2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n",
"abaqus.semantic.invalid_element_data",
},
};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-exact-mesh-data-" + std::string{test_case.name} + ".inp",
std::string{test_case.mesh} +
"*ELSET, ELSET=Beam\n1\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n210000.0, 0.3\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
"MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n0.0, 1.0, 0.0\n"
"*STEP\n*STATIC\n*END STEP\n",
};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, test_case.code));
}
}
TEST(DeckToDomain, ReportsMeshDataErrorsAtTheOffendingRows) {
const TemporaryDeck invalid_coordinate{
"fesa-node-coordinate-source.inp",
"*NODE\n"
"1, invalid, 0.0, 0.0\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const TemporaryDeck missing_node{
"fesa-element-node-source.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\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"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto coordinate_result = parse_and_map(invalid_coordinate.path());
const auto missing_node_result = parse_and_map(missing_node.path());
const fesa::Diagnostic* coordinate =
find_diagnostic(coordinate_result, "abaqus.semantic.invalid_number");
ASSERT_NE(coordinate, nullptr);
ASSERT_TRUE(coordinate->source.has_value());
EXPECT_EQ(coordinate->source->line, 2U);
const fesa::Diagnostic* node =
find_diagnostic(missing_node_result, "abaqus.semantic.missing_node");
ASSERT_NE(node, nullptr);
ASSERT_TRUE(node->source.has_value());
EXPECT_EQ(node->source->line, 4U);
}
TEST(DeckToDomain, AcceptsCaseInsensitiveElementAndSectionValues) {
const TemporaryDeck input{
"fesa-case-insensitive-enums.inp",
"*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"
"210000.0, 0.3\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"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
EXPECT_EQ(result.domain->beam_elements().size(), 1U);
}
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, RejectsInvalidRecordsInUnreferencedParts) {
struct Case final {
std::string_view name;
std::string_view unused_records;
std::string_view code;
std::size_t line;
};
const Case cases[]{
{
"node-surplus",
"*NODE\n1, 0.0, 0.0, 0.0, 9.0\n",
"abaqus.semantic.invalid_node_data",
3U,
},
{
"element-type",
"*ELEMENT, TYPE=B32\n1, 1, 2\n",
"abaqus.semantic.unsupported_element",
2U,
},
{
"duplicate-node",
"*NODE\n1, 0.0, 0.0, 0.0\n1, 1.0, 0.0, 0.0\n",
"abaqus.semantic.duplicate_node_label",
4U,
},
{
"missing-node",
"*NODE\n1, 0.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31\n1, 1, 2\n",
"abaqus.semantic.missing_node",
5U,
},
{
"section-data",
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, "
"MATERIAL=Steel\n"
"0.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n",
"abaqus.semantic.invalid_section_data",
3U,
},
};
const std::string active_model{
"*PART, NAME=BeamPart\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"2, 1.0, 0.0, 0.0\n"
"*ELEMENT, TYPE=B31, ELSET=Beam\n"
"1, 1, 2\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*STEP\n"
"*STATIC\n"
"*END STEP\n"};
for (const auto& test_case : cases) {
const TemporaryDeck input{
"fesa-inactive-part-" + std::string{test_case.name} + ".inp",
"*PART, NAME=Unused\n" + std::string{test_case.unused_records} +
"*END PART\n" + active_model,
};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, test_case.code);
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, test_case.line);
}
}
TEST(ActiveInstance, RejectsInstanceTransformWithSourceDiagnostic) {
const TemporaryDeck input{
"fesa-instance-transform.inp",
"*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, 5U);
}
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());
const auto diagnostic = std::ranges::find(
result.diagnostics,
std::string_view{"abaqus.semantic.instance_count"},
&fesa::Diagnostic::code);
ASSERT_NE(diagnostic, result.diagnostics.end());
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 6U);
}
TEST(StepMapping, MapsOneStaticStepAndRejectsUnsupportedConfigurations) {
const auto valid =
parse_and_map(fixture_path("valid/noop_directives.inp"));
ASSERT_TRUE(valid.domain.has_value());
EXPECT_TRUE(valid.diagnostics.empty());
EXPECT_EQ(valid.domain->step().name, "Step-1");
struct InvalidCase final {
std::string_view name;
std::string_view contents;
std::string_view code;
std::size_t line;
};
const InvalidCase invalid_cases[]{
{
"fesa-step-multiple.inp",
"*STEP, NAME=First\n"
"*STATIC\n"
"*END STEP\n"
"*STEP, NAME=Second\n"
"*STATIC\n"
"*END STEP\n",
"abaqus.semantic.step_count",
4U,
},
{
"fesa-step-nlgeom.inp",
"*STEP, NLGEOM=YES\n"
"*STATIC\n"
"*END STEP\n",
"abaqus.semantic.unsupported_step_option",
1U,
},
{
"fesa-static-invalid.inp",
"*STEP\n"
"*STATIC\n"
"1.0, -1.0\n"
"*END STEP\n",
"abaqus.semantic.invalid_static_data",
3U,
},
{
"fesa-static-extra-row.inp",
"*STEP\n"
"*STATIC\n"
"1.0\n"
"2.0\n"
"*END STEP\n",
"abaqus.semantic.invalid_static_data",
4U,
},
};
for (const InvalidCase& test_case : invalid_cases) {
const TemporaryDeck input{test_case.name, test_case.contents};
const auto result = parse_and_map(input.path());
SCOPED_TRACE(test_case.name);
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, test_case.code);
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, test_case.line);
}
}
TEST(StepMapping, RejectsModelDataInsideStep) {
const TemporaryDeck input{
"fesa-node-inside-step.inp",
"*STEP\n"
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.syntax.invalid_node_scope");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 2U);
}
TEST(StepMapping, RejectsBoundaryAfterCompletedStep) {
const TemporaryDeck input{
"fesa-boundary-after-step.inp",
"*STEP\n"
"*STATIC\n"
"*END STEP\n"
"*BOUNDARY\n"
"1, 1\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.syntax.invalid_boundary_scope");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 4U);
}
TEST(Boundary, CanonicalizesIdenticalGlobalAndStepPrescriptions) {
const TemporaryDeck input{
"fesa-boundary-canonical.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*BOUNDARY\n"
"1, 1, 3, 2.5\n"
"*STEP\n"
"*STATIC\n"
"*BOUNDARY\n"
"1, 1, 3, 2.5\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
const auto& prescribed = result.domain->step().prescribed_dofs;
ASSERT_EQ(prescribed.size(), 3U);
for (std::size_t index = 0; index < prescribed.size(); ++index) {
EXPECT_EQ(prescribed[index].node, fesa::NodeId{0});
EXPECT_EQ(prescribed[index].dof, index + 1U);
EXPECT_DOUBLE_EQ(prescribed[index].value, 2.5);
}
}
TEST(Boundary, HierarchicalTargetsRequireAssemblySets) {
const TemporaryDeck input{
"fesa-hierarchical-part-set-target.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, ELSET=Beam\n"
"1, 1, 2\n"
"*NSET, NSET=Fixed\n"
"1\n"
"*ELSET, ELSET=Beam\n"
"1\n"
"*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel\n"
"1.0, 1.0, 0.0, 1.0, 1.0\n"
"0.0, 1.0, 0.0\n"
"*END PART\n"
"*ASSEMBLY, NAME=RootAssembly\n"
"*INSTANCE, NAME=Beam-1, PART=BeamPart\n"
"*END INSTANCE\n"
"*END ASSEMBLY\n"
"*MATERIAL, NAME=Steel\n"
"*ELASTIC\n"
"210000.0, 0.3\n"
"*STEP\n"
"*STATIC\n"
"*BOUNDARY\n"
"Fixed, 1, 6\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
EXPECT_FALSE(result.domain.has_value());
EXPECT_TRUE(has_diagnostic(result, "abaqus.semantic.missing_node_target"));
}
TEST(Boundary, ReportsConflictAtTheConflictingDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/boundary_conflict.inp"));
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.semantic.conflicting_boundary");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 10U);
}
TEST(Cload, SumsForceAndMomentComponentsInInputOrder) {
const TemporaryDeck input{
"fesa-cload-sum.inp",
"*NODE\n"
"1, 0.0, 0.0, 0.0\n"
"*STEP, NAME=Load\n"
"*STATIC\n"
"*CLOAD\n"
"1, 1, 2.0\n"
"1, 1, -0.5\n"
"1, 6, 4.0\n"
"*END STEP\n"};
const auto result = parse_and_map(input.path());
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
ASSERT_EQ(result.domain->step().nodal_loads.size(), 1U);
const auto& load = result.domain->step().nodal_loads.front();
EXPECT_EQ(load.node, fesa::NodeId{0});
EXPECT_EQ(
load.values,
(std::array<double, 6>{1.5, 0.0, 0.0, 0.0, 0.0, 4.0}));
}
TEST(Cload, ReportsInvalidDofAtTheDataRow) {
const auto result =
parse_and_map(fixture_path("invalid/cload_invalid_dof.inp"));
EXPECT_FALSE(result.domain.has_value());
const fesa::Diagnostic* diagnostic =
find_diagnostic(result, "abaqus.semantic.invalid_dof");
ASSERT_NE(diagnostic, nullptr);
ASSERT_TRUE(diagnostic->source.has_value());
EXPECT_EQ(diagnostic->source->line, 6U);
}
TEST(SuppliedCantilever, NormalizesReferenceModelThroughPublicParserAndMapper) {
const std::filesystem::path path =
std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
"reference" / "cantilever beam" / "cantilever beam fesa.inp";
const auto result = parse_and_map(path);
ASSERT_TRUE(result.domain.has_value());
EXPECT_TRUE(result.diagnostics.empty());
const fesa::Domain& domain = *result.domain;
EXPECT_EQ(domain.nodes().size(), 11U);
EXPECT_EQ(domain.beam_elements().size(), 10U);
EXPECT_EQ(domain.step().prescribed_dofs.size(), 6U);
ASSERT_EQ(domain.step().nodal_loads.size(), 1U);
const auto node = std::ranges::find_if(
domain.nodes(),
[](const fesa::Node& candidate) {
return candidate.origin.local_label == 11;
});
ASSERT_NE(node, domain.nodes().end());
EXPECT_EQ(domain.step().nodal_loads.front().node, node->id);
EXPECT_EQ(
domain.step().nodal_loads.front().values,
(std::array<double, 6>{0.0, 0.0, -1.0e6, 0.0, 0.0, 0.0}));
}
} // namespace