Files
FESADev/tests/unit/io/abaqus/domain_mapper_test.cpp
T

1005 lines
40 KiB
C++

#include "fesa/io/abaqus/domain_mapper.h"
#include <gtest/gtest.h>
#include <algorithm>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <optional>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/elements/element.h"
#include "fesa/elements/element_definition.h"
#include "fesa/io/abaqus/input_reader.h"
#include "fesa/math/vector3.h"
namespace {
class TemporaryInputFile {
public:
TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-domain-mapper-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) {
throw std::runtime_error{"Unable to create domain mapper fixture."};
}
}
~TemporaryInputFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
fesa::Result<fesa::Domain> MapText(const std::string& stem,
const std::string& content) {
const TemporaryInputFile input{stem, content};
auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
if (!parsed.HasValue()) {
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
}
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
}
std::string ReadExactBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
if (!stream) {
throw std::runtime_error{"Unable to read legacy mapper fixture."};
}
return std::string{std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
}
std::filesystem::path RepositoryRoot() {
auto path = std::filesystem::path{__FILE__}.parent_path();
for (int parent = 0; parent < 4; ++parent) {
path = path.parent_path();
}
return path;
}
const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status,
const std::string& code) {
const auto found =
std::find_if(status.Diagnostics().begin(), status.Diagnostics().end(),
[&code](const fesa::Diagnostic& diagnostic) {
return diagnostic.code == code;
});
return found == status.Diagnostics().end() ? nullptr : &*found;
}
std::string ReplaceOnce(std::string text, const std::string& from,
const std::string& to) {
const auto position = text.find(from);
if (position == std::string::npos) {
throw std::logic_error{"Mapper test mutation source was not found."};
}
text.replace(position, from.size(), to);
return text;
}
std::string MinimalDeck() {
return R"inp(*Part, name=BeamPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
*Element, type=B33
1, 1, 2
*Elset, elset=BeamSet
1
*Beam General Section, elset=BeamSet, material=Steel, section=GENERAL
1., 1., 0., 1., 1.
0., 1., 0.
*End Part
*Assembly, name=Assembly
*Instance, name=Beam-1, part=BeamPart
*End Instance
*Nset, nset=Root, instance=Beam-1
1
*Nset, nset=Tip, instance=Beam-1
2
*End Assembly
*Material, name=Steel
*Elastic
100., 0.25
*Boundary
Root, 1, 6
*Step, name=Load, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
*Cload
Tip, 2, -1.
*End Step
)inp";
}
std::string ShellDeck() {
return R"inp(*Part, name=ShellPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
5, 2., 0., 0.
6, 2., 1., 0.
*Element, type=S4
0010, 1, 2, 3, 4
*Element, type=S4R
0020, 2, 5, 6, 3
*Elset, elset=ShellS4
10
*Elset, elset=ShellS4R
20
*Shell Section, elset=ShellS4, material=Steel
0.1, 5
*Shell Section, elset=ShellS4R, material=Aluminum
0.2
*End Part
*Assembly, name=Assembly
*Instance, name=First, part=ShellPart
*End Instance
*Instance, name=Second, part=ShellPart
*End Instance
*Nset, nset=RootFirst, instance=First
1
*Nset, nset=TipSecond, instance=Second
6
*End Assembly
*Material, name=Steel
*Elastic
210000., 0.3
*Material, name=Aluminum
*Elastic
70000., 0.25
*Boundary
RootFirst, 1, 6
*Step, name=Load, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
*Cload
TipSecond, 6, 1.
*End Step
)inp";
}
std::string SupportedInventoryDeck(bool include_no_ops) {
const std::string preprint =
include_no_ops ? "*Preprint, echo=NO, model=NO, history=NO, contact=NO\n"
: "";
const std::string shear =
include_no_ops ? "*Transverse Shear Stiffness\n1., 2., 3.\n" : "";
const std::string outputs = include_no_ops
? R"inp(*Restart, write, frequency=0
*Output, field
*Node Output
U, RF
*Element Output, directions=YES
S, SF
*Contact Output, variable=PRESELECT
*Output, history, variable=PRESELECT
)inp"
: "";
return std::string{R"inp(*hEaDiNg
Every supported keyword
)inp"} + preprint +
R"inp(*pArT, NaMe=BeamPart
*nOdE
0001, 0., 0., 0.
0002, 2., 0., 0.
*eLeMeNt, TyPe=b33
0007, 0001, 0002
*nSeT, NsEt=RootLocal
0001
*Nset, nset=AllLocal, generate
1, 2, 1
*Elset, elset=BeamExplicit
0007
*eLsEt, ElSeT=BeamAll, GeNeRaTe
7, 7, 1
*Beam General Section, ELSET=beamall, MATERIAL=steel, SECTION=general
2., 3., 0., 4., 5.
0., 1., 0.
*Section Points
-0.5, 0.25
0.5, -0.25
)inp" + shear +
R"inp(*eNd PaRt
*aSsEmBlY, name=Assembly
*iNsTaNcE, NAME=Beam-1, PART=beampart
*eNd InStAnCe
*Nset, nset=RootAssembly, instance=beam-1
0001
*Elset, elset=BeamAssembly, instance=BEAM-1
0007
*eNd AsSeMbLy
*mAtErIaL, NaMe=Steel
*eLaStIc
210000., 0.75
*bOuNdArY
rootassembly, 1, 1, 0.125
*sTeP, name=User-Spelling, nLgEoM=no
*sTaTiC
0.25, 1.5, 0.01, 1.5
*Boundary
RootAssembly, 2, 2
*cLoAd
RootAssembly, 6, -12.5
)inp" + outputs +
"*eNd StEp\n";
}
} // namespace
// C-DUP-002
// C-MODEL-002
TEST(InpDomainMapping, MapsEverySupportedKeywordAndLegacyDeck) {
auto result = MapText("supported-inventory", SupportedInventoryDeck(true));
ASSERT_TRUE(result.HasValue());
const fesa::Domain& domain = result.Value();
ASSERT_EQ(domain.Nodes().size(), 2U);
EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "Beam-1");
EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 1);
EXPECT_EQ(domain.Nodes()[0].source_id.source_label_text, "0001");
EXPECT_EQ(domain.Nodes()[1].source_id.source_label_text, "0002");
EXPECT_DOUBLE_EQ(domain.Nodes()[1].coordinates[0], 2.0);
ASSERT_EQ(domain.Elements().Size(), 1U);
const fesa::ElementDefinition& element = domain.Elements()[0U];
EXPECT_EQ(element.Kind(), fesa::ElementDefinitionKind::kEulerBeam3D);
EXPECT_EQ(element.SourceId().source_label_text, "0007");
EXPECT_EQ(element.SourceElementType(), "B33");
EXPECT_EQ(element.NodeIndices(), (std::vector<fesa::EntityIndex>{0U, 1U}));
EXPECT_EQ(&element, &domain.BeamElements()[0U]);
ASSERT_EQ(domain.Materials().Size(), 1U);
EXPECT_EQ(domain.Materials()[0U].Kind(),
fesa::MaterialKind::kIsotropicLinearElastic);
EXPECT_EQ(domain.LinearElasticMaterials()[0U].name, "Steel");
EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].youngs_modulus,
210000.0);
EXPECT_DOUBLE_EQ(domain.LinearElasticMaterials()[0U].poisson_ratio, 0.75);
ASSERT_EQ(domain.Sections().Size(), 1U);
const auto& section = domain.Sections()[0];
EXPECT_DOUBLE_EQ(section.area, 2.0);
EXPECT_DOUBLE_EQ(section.i11, 3.0);
EXPECT_DOUBLE_EQ(section.i12, 0.0);
EXPECT_DOUBLE_EQ(section.i22, 4.0);
EXPECT_DOUBLE_EQ(section.torsional_constant, 5.0);
EXPECT_EQ(section.first_axis, (std::array<double, 3>{0.0, 1.0, 0.0}));
EXPECT_EQ(section.section_points,
(std::vector<std::array<double, 2>>{{-0.5, 0.25}, {0.5, -0.25}}));
EXPECT_EQ(domain.BeamElements()[0U].material_index, 0U);
EXPECT_EQ(domain.BeamElements()[0U].section_index, 0U);
ASSERT_EQ(domain.Steps().Size(), 1U);
const auto& step = domain.Steps()[0];
EXPECT_EQ(step.Name(), "Step-1");
EXPECT_DOUBLE_EQ(step.InitialIncrement(), 0.25);
EXPECT_DOUBLE_EQ(step.TimePeriod(), 1.5);
EXPECT_DOUBLE_EQ(step.MinimumIncrement(), 0.01);
EXPECT_DOUBLE_EQ(step.MaximumIncrement(), 1.5);
ASSERT_EQ(step.PrescribedDisplacements().Size(), 2U);
EXPECT_EQ(step.PrescribedDisplacements()[0U].Target().target_name_or_label,
"rootassembly");
EXPECT_EQ(step.PrescribedDisplacements()[0U].Component(),
fesa::DofComponent::kUx);
EXPECT_DOUBLE_EQ(step.PrescribedDisplacements()[0U].PrescribedValue(), 0.125);
EXPECT_EQ(step.PrescribedDisplacements()[1U].Component(),
fesa::DofComponent::kUy);
EXPECT_DOUBLE_EQ(step.PrescribedDisplacements()[1U].PrescribedValue(), 0.0);
ASSERT_EQ(step.ConcentratedLoads().Size(), 1U);
EXPECT_EQ(step.ConcentratedLoads()[0U].Target().target_name_or_label,
"RootAssembly");
EXPECT_DOUBLE_EQ(step.ConcentratedLoads()[0U].GlobalComponents()[5U], -12.5);
EXPECT_EQ(domain.Warnings().size(), 8U);
const auto legacy_path = RepositoryRoot() / "reference" / "cantilever beam" /
"cantilever beam.inp";
const auto bytes_before = ReadExactBytes(legacy_path);
const auto timestamp_before = std::filesystem::last_write_time(legacy_path);
auto parsed_legacy = fesa::AbaqusInputReader{}.Read(legacy_path);
ASSERT_TRUE(parsed_legacy.HasValue());
auto legacy = fesa::AbaqusDomainMapper{}.Map(parsed_legacy.Value());
ASSERT_TRUE(legacy.HasValue());
EXPECT_EQ(legacy.Value().Nodes().size(), 11U);
EXPECT_EQ(legacy.Value().Elements().Size(), 10U);
EXPECT_EQ(legacy.Value().Materials().Size(), 1U);
EXPECT_EQ(legacy.Value().Sections().Size(), 1U);
EXPECT_EQ(legacy.Value().Steps().Size(), 1U);
EXPECT_EQ(legacy.Value().Warnings().size(), 7U);
EXPECT_EQ(ReadExactBytes(legacy_path), bytes_before);
EXPECT_EQ(std::filesystem::last_write_time(legacy_path), timestamp_before);
}
TEST(InpDomainMapping,
ExpandsSetsAndMultipleIdentityInstancesDeterministically) {
const std::string deck = R"inp(*Part, name=P
*Node
10, 0., 0., 0.
20, 1., 0., 0.
*Element, type=B33
30, 10, 20
*Nset, nset=Ends, generate
10, 20, 10
*Elset, elset=AllElements
30
*Beam General Section, elset=AllElements, material=M, section=GENERAL
1., 2., 0., 3., 4.
0., 1., 0.
*End Part
*Assembly, name=A
*Instance, name=First, part=P
*End Instance
*Instance, name=Second, part=P
*End Instance
*Nset, nset=OnlySecond, instance=Second
20
*Elset, elset=OnlySecondBeam, instance=Second
30
*End Assembly
*Material, name=M
*Elastic
1000., 0.25
*Step
*Static
1., 1., 1., 1.
*Boundary
OnlySecond, 1, 1
*Cload
OnlySecond, 2, 5.
*End Step
)inp";
auto result = MapText("multiple-instances", deck);
ASSERT_TRUE(result.HasValue());
const fesa::Domain& domain = result.Value();
ASSERT_EQ(domain.Nodes().size(), 4U);
EXPECT_EQ(domain.Nodes()[0].source_id.instance_name, "First");
EXPECT_EQ(domain.Nodes()[0].source_id.source_label, 10);
EXPECT_EQ(domain.Nodes()[1].source_id.instance_name, "First");
EXPECT_EQ(domain.Nodes()[1].source_id.source_label, 20);
EXPECT_EQ(domain.Nodes()[2].source_id.instance_name, "Second");
EXPECT_EQ(domain.Nodes()[2].source_id.source_label, 10);
EXPECT_EQ(domain.Nodes()[3].source_id.instance_name, "Second");
EXPECT_EQ(domain.Nodes()[3].source_id.source_label, 20);
ASSERT_EQ(domain.BeamElements().Size(), 2U);
EXPECT_EQ(domain.BeamElements()[0].source_id.instance_name, "First");
EXPECT_EQ(domain.BeamElements()[0].node_indices,
(std::array<fesa::EntityIndex, 2>{0U, 1U}));
EXPECT_EQ(domain.BeamElements()[1].source_id.instance_name, "Second");
EXPECT_EQ(domain.BeamElements()[1].node_indices,
(std::array<fesa::EntityIndex, 2>{2U, 3U}));
ASSERT_EQ(domain.NodeSets().size(), 3U);
EXPECT_EQ(domain.NodeSets()[0].name, "Ends");
EXPECT_EQ(domain.NodeSets()[0].instance_name,
std::optional<std::string>{"First"});
EXPECT_EQ(domain.NodeSets()[0].node_indices,
(std::vector<fesa::EntityIndex>{0U, 1U}));
EXPECT_EQ(domain.NodeSets()[1].instance_name,
std::optional<std::string>{"Second"});
EXPECT_EQ(domain.NodeSets()[1].node_indices,
(std::vector<fesa::EntityIndex>{2U, 3U}));
EXPECT_EQ(domain.NodeSets()[2].name, "OnlySecond");
EXPECT_EQ(domain.NodeSets()[2].node_indices,
(std::vector<fesa::EntityIndex>{3U}));
ASSERT_EQ(domain.ElementSets().size(), 3U);
EXPECT_EQ(domain.ElementSets()[0].instance_name,
std::optional<std::string>{"First"});
EXPECT_EQ(domain.ElementSets()[0].element_indices,
(std::vector<fesa::EntityIndex>{0U}));
EXPECT_EQ(domain.ElementSets()[1].instance_name,
std::optional<std::string>{"Second"});
EXPECT_EQ(domain.ElementSets()[1].element_indices,
(std::vector<fesa::EntityIndex>{1U}));
EXPECT_EQ(domain.ElementSets()[2].name, "OnlySecondBeam");
EXPECT_EQ(domain.ElementSets()[2].element_indices,
(std::vector<fesa::EntityIndex>{1U}));
ASSERT_EQ(domain.Steps().Size(), 1U);
EXPECT_EQ(domain.Steps()[0]
.PrescribedDisplacements()[0U]
.Target()
.target_name_or_label,
"OnlySecond");
EXPECT_EQ(
domain.Steps()[0].ConcentratedLoads()[0U].Target().target_name_or_label,
"OnlySecond");
auto direct =
MapText("direct-node-labels",
ReplaceOnce(ReplaceOnce(MinimalDeck(), "Root, 1, 6", "1, 1, 6"),
"Tip, 2, -1.", "2, 2, -1."));
ASSERT_TRUE(direct.HasValue());
EXPECT_EQ(direct.Value()
.Steps()[0]
.PrescribedDisplacements()[0U]
.Target()
.target_name_or_label,
"1");
EXPECT_EQ(direct.Value()
.Steps()[0]
.ConcentratedLoads()[0U]
.Target()
.target_name_or_label,
"2");
auto above_thresholds = MapText(
"above-geometry-thresholds",
ReplaceOnce(
ReplaceOnce(MinimalDeck(), "2, 1., 0., 0.", "2, 2e-12, 0., 0."),
"0., 1., 0.", "1., 2e-12, 0."));
ASSERT_TRUE(above_thresholds.HasValue());
auto large_finite = MapText(
"large-finite-geometry",
ReplaceOnce(ReplaceOnce(ReplaceOnce(MinimalDeck(), "1, 0., 0., 0.",
"1, 1e308, 0., 0."),
"2, 1., 0., 0.", "2, 1e308, 1e297, 0."),
"0., 1., 0.", "1e308, 0., 0."));
ASSERT_TRUE(large_finite.HasValue());
const auto& large_domain = large_finite.Value();
ASSERT_EQ(large_domain.Nodes().size(), 2U);
const fesa::Vector3 large_delta =
fesa::Vector3{large_domain.Nodes()[1U].coordinates} -
fesa::Vector3{large_domain.Nodes()[0U].coordinates};
EXPECT_TRUE(large_delta.IsFinite());
EXPECT_TRUE(std::isfinite(large_delta.Norm()));
EXPECT_DOUBLE_EQ(large_delta.Y(), 1.0e297);
const fesa::Vector3 large_guide{large_domain.Sections()[0U].first_axis};
EXPECT_TRUE(large_guide.IsFinite());
EXPECT_TRUE(std::isfinite(large_guide.Norm()));
}
TEST(InpDomainMapping, KeepsNodeAndElementSetNamesInSeparateNamespaces) {
auto shared_name = MapText(
"separate-part-set-namespaces",
ReplaceOnce(
ShellDeck(), "*Elset, elset=ShellS4\n10",
"*Nset, nset=ShellS4\n1, 2, 3, 4\n*Elset, elset=ShellS4\n10"));
ASSERT_TRUE(shared_name.HasValue());
const auto& domain = shared_name.Value();
EXPECT_TRUE(std::any_of(
domain.NodeSets().begin(), domain.NodeSets().end(),
[](const fesa::NodeSet& set) { return set.name == "ShellS4"; }));
EXPECT_TRUE(std::any_of(
domain.ElementSets().begin(), domain.ElementSets().end(),
[](const fesa::ElementSet& set) { return set.name == "ShellS4"; }));
auto duplicate_node_set =
MapText("duplicate-part-node-set",
ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10",
"*Nset, nset=Shared\n1\n*Nset, nset=Shared\n2\n"
"*Elset, elset=ShellS4\n10"));
ASSERT_FALSE(duplicate_node_set.HasValue());
EXPECT_NE(FindDiagnostic(duplicate_node_set.GetStatus(), "duplicate-entity"),
nullptr);
auto duplicate_element_set = MapText(
"duplicate-part-element-set",
ReplaceOnce(ShellDeck(), "*Elset, elset=ShellS4\n10",
"*Elset, elset=Repeated\n10\n*Elset, elset=Repeated\n20\n"
"*Elset, elset=ShellS4\n10"));
ASSERT_FALSE(duplicate_element_set.HasValue());
EXPECT_NE(
FindDiagnostic(duplicate_element_set.GetStatus(), "duplicate-entity"),
nullptr);
auto assembly_shared_name = MapText(
"separate-assembly-set-namespaces",
ReplaceOnce(
ReplaceOnce(MinimalDeck(), "*Elset, elset=BeamSet\n1",
"*Nset, nset=Root\n1, 2\n*Elset, elset=BeamSet\n1"),
"*Nset, nset=Root, instance=Beam-1\n1",
"*Nset, nset=Root, instance=Beam-1\n1\n"
"*Elset, elset=Root, instance=Beam-1\n1"));
ASSERT_TRUE(assembly_shared_name.HasValue());
const auto& assembly_domain = assembly_shared_name.Value();
const auto root_node_set_count = std::count_if(
assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(),
[](const fesa::NodeSet& set) { return set.name == "Root"; });
EXPECT_EQ(root_node_set_count, 1);
const auto root_node_set = std::find_if(
assembly_domain.NodeSets().begin(), assembly_domain.NodeSets().end(),
[](const fesa::NodeSet& set) { return set.name == "Root"; });
ASSERT_NE(root_node_set, assembly_domain.NodeSets().end());
EXPECT_EQ(root_node_set->node_indices, (std::vector<fesa::EntityIndex>{0U}));
EXPECT_EQ(std::count_if(
assembly_domain.ElementSets().begin(),
assembly_domain.ElementSets().end(),
[](const fesa::ElementSet& set) { return set.name == "Root"; }),
1);
}
TEST(InpDomainMapping, NoOpAllowlistWarnsWithoutSemanticEffect) {
auto plain = MapText("without-no-ops", SupportedInventoryDeck(false));
auto with_no_ops = MapText("with-no-ops", SupportedInventoryDeck(true));
ASSERT_TRUE(plain.HasValue());
ASSERT_TRUE(with_no_ops.HasValue());
EXPECT_TRUE(plain.Value().Warnings().empty());
ASSERT_EQ(with_no_ops.Value().Warnings().size(), 8U);
EXPECT_EQ(with_no_ops.Value().Warnings()[0].code, "ignored-input-keyword");
EXPECT_EQ(with_no_ops.Value().Warnings()[0].keyword, "PREPRINT");
EXPECT_EQ(with_no_ops.Value().Warnings()[1].keyword,
"TRANSVERSE SHEAR STIFFNESS");
EXPECT_EQ(with_no_ops.Value().Warnings()[2].keyword, "RESTART");
EXPECT_EQ(with_no_ops.Value().Warnings()[3].keyword, "OUTPUT");
EXPECT_EQ(with_no_ops.Value().Warnings()[4].keyword, "NODE OUTPUT");
EXPECT_EQ(with_no_ops.Value().Warnings()[5].keyword, "ELEMENT OUTPUT");
EXPECT_EQ(with_no_ops.Value().Warnings()[6].keyword, "CONTACT OUTPUT");
EXPECT_EQ(with_no_ops.Value().Warnings()[7].keyword, "OUTPUT");
for (const auto& warning : with_no_ops.Value().Warnings()) {
EXPECT_EQ(warning.severity, fesa::Severity::kWarning);
}
EXPECT_EQ(with_no_ops.Value().Nodes().size(), plain.Value().Nodes().size());
EXPECT_EQ(with_no_ops.Value().Elements().Size(),
plain.Value().Elements().Size());
EXPECT_EQ(with_no_ops.Value().Materials().Size(),
plain.Value().Materials().Size());
EXPECT_EQ(with_no_ops.Value().Sections().Size(),
plain.Value().Sections().Size());
EXPECT_EQ(with_no_ops.Value().NodeSets().size(),
plain.Value().NodeSets().size());
EXPECT_EQ(with_no_ops.Value().ElementSets().size(),
plain.Value().ElementSets().size());
EXPECT_EQ(with_no_ops.Value().Steps().Size(), plain.Value().Steps().Size());
EXPECT_EQ(with_no_ops.Value().Steps()[0].BoundaryConditions().size(),
plain.Value().Steps()[0].BoundaryConditions().size());
EXPECT_EQ(with_no_ops.Value().Steps()[0].Loads().size(),
plain.Value().Steps()[0].Loads().size());
}
// MITC4-MAP-001
// C-MODEL-002
TEST(InpDomainMapping, MapsS4AndS4rThroughOneMitc4Identity) {
auto result = MapText("mitc4-map-001", ShellDeck());
ASSERT_TRUE(result.HasValue());
const fesa::Domain& domain = result.Value();
EXPECT_TRUE(domain.BeamElements().Empty());
ASSERT_EQ(domain.Elements().Size(), 4U);
EXPECT_EQ(domain.Elements()[0U].Kind(),
fesa::ElementDefinitionKind::kMitc4Shell);
EXPECT_EQ(domain.Elements()[0U].SourceElementType(), "S4");
EXPECT_EQ(domain.Elements()[1U].SourceElementType(), "S4R");
EXPECT_EQ(&domain.Elements()[0U], &domain.ShellElements()[0U]);
ASSERT_EQ(domain.ShellElements().Size(), 4U);
EXPECT_EQ(domain.ShellElements()[0].source_id.instance_name, "First");
EXPECT_EQ(domain.ShellElements()[0].source_id.source_label_text, "0010");
EXPECT_EQ(domain.ShellElements()[0].source_type,
fesa::ShellSourceElementType::kS4);
EXPECT_EQ(domain.ShellElements()[0].node_indices,
(std::array<fesa::EntityIndex, 4>{0U, 1U, 2U, 3U}));
EXPECT_EQ(domain.ShellElements()[0].section_index, 0U);
EXPECT_EQ(domain.ShellElements()[0].material_index, 0U);
EXPECT_EQ(domain.ShellElements()[1].source_id.instance_name, "First");
EXPECT_EQ(domain.ShellElements()[1].source_type,
fesa::ShellSourceElementType::kS4r);
EXPECT_EQ(domain.ShellElements()[1].node_indices,
(std::array<fesa::EntityIndex, 4>{1U, 4U, 5U, 2U}));
EXPECT_EQ(domain.ShellElements()[1].section_index, 1U);
EXPECT_EQ(domain.ShellElements()[1].material_index, 1U);
EXPECT_EQ(domain.ShellElements()[2].source_id.instance_name, "Second");
EXPECT_EQ(domain.ShellElements()[2].node_indices,
(std::array<fesa::EntityIndex, 4>{6U, 7U, 8U, 9U}));
EXPECT_EQ(domain.ShellElements()[3].source_id.instance_name, "Second");
EXPECT_EQ(fesa::kMitc4InternalFormulation, std::string_view{"FESA-MITC4"});
ASSERT_EQ(domain.ShellSections().Size(), 2U);
EXPECT_EQ(domain.ShellSections()[0].name, "ShellS4");
EXPECT_DOUBLE_EQ(domain.ShellSections()[0].thickness, 0.1);
EXPECT_EQ(domain.ShellSections()[0].material_index, 0U);
EXPECT_EQ(domain.ShellSections()[1].name, "ShellS4R");
EXPECT_DOUBLE_EQ(domain.ShellSections()[1].thickness, 0.2);
EXPECT_EQ(domain.ShellSections()[1].material_index, 1U);
ASSERT_EQ(domain.ShellNodeInitialFrames().size(), domain.Nodes().size());
EXPECT_EQ(domain.ShellNodeInitialFrames()[0].node_index, 0U);
EXPECT_EQ(domain.ShellNodeInitialFrames()[0].director,
(std::array<double, 3>{0.0, 0.0, 1.0}));
EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_a,
(std::array<double, 3>{1.0, 0.0, 0.0}));
EXPECT_EQ(domain.ShellNodeInitialFrames()[0].tangent_b,
(std::array<double, 3>{0.0, 1.0, 0.0}));
ASSERT_EQ(domain.Steps().Size(), 1U);
ASSERT_EQ(domain.Steps()[0].PrescribedDisplacements().Size(), 6U);
EXPECT_EQ(domain.Steps()[0].PrescribedDisplacements()[5U].Component(),
fesa::DofComponent::kUrz);
ASSERT_EQ(domain.Steps()[0].ConcentratedLoads().Size(), 1U);
EXPECT_DOUBLE_EQ(
domain.Steps()[0].ConcentratedLoads()[0U].GlobalComponents()[5U], 1.0);
}
// MITC4-MAP-002
TEST(InpDomainMapping, RejectsInvalidShellAssignmentsAndProperties) {
struct InvalidCase {
std::string name;
std::string deck;
std::string expected_code;
fesa::FailureCategory category;
};
const std::string base = ShellDeck();
const std::vector<InvalidCase> cases{
{"unresolved-material",
ReplaceOnce(base, "material=Steel", "material=Missing"),
"unresolved-shell-section", fesa::FailureCategory::kInput},
{"unresolved-elset",
ReplaceOnce(base, "elset=ShellS4, material=Steel",
"elset=Missing, material=Steel"),
"unresolved-shell-section", fesa::FailureCategory::kInput},
{"missing-assignment",
ReplaceOnce(base,
"*Shell Section, elset=ShellS4R, material=Aluminum\n0.2\n",
""),
"invalid-shell-section-assignment", fesa::FailureCategory::kInput},
{"conflicting-assignment",
ReplaceOnce(base, "elset=ShellS4R, material=Aluminum",
"elset=ShellS4, material=Aluminum"),
"invalid-shell-section-assignment", fesa::FailureCategory::kInput},
{"invalid-thickness",
ReplaceOnce(base, "0.2\n*End Part", "0.\n*End Part"),
"invalid-shell-thickness", fesa::FailureCategory::kModel},
{"invalid-material", ReplaceOnce(base, "70000., 0.25", "70000., 0.5"),
"invalid-shell-material", fesa::FailureCategory::kModel}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.name);
auto result = MapText("mitc4-map-002-" + test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), test_case.category);
ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code),
nullptr);
}
}
// MITC4-MAP-003
TEST(InpDomainMapping, RejectsInvalidShellConnectivityOptionsAndMixedModels) {
struct InvalidCase {
std::string name;
std::string deck;
std::string expected_code;
};
const std::string base = ShellDeck();
const std::vector<InvalidCase> cases{
{"wrong-arity", ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3"),
"invalid-shell-connectivity"},
{"repeated-node",
ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 2, 4"),
"invalid-shell-connectivity"},
{"dangling-node",
ReplaceOnce(base, "0010, 1, 2, 3, 4", "0010, 1, 2, 3, 99"),
"invalid-shell-connectivity"},
{"unsupported-section-option",
ReplaceOnce(base, "material=Steel\n0.1",
"material=Steel, offset=0.1\n0.1"),
"unsupported-shell-section-option"},
{"unsupported-element", ReplaceOnce(base, "type=S4R", "type=S8R"),
"unsupported-element-formulation"},
{"mixed-beam-shell",
ReplaceOnce(base, "0020, 2, 5, 6, 3\n*Elset",
"0020, 2, 5, 6, 3\n*Element, type=B33\n30, 1, 2\n*Elset"),
"unsupported-mixed-element-model"}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.name);
auto result = MapText("mitc4-map-003-" + test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code),
nullptr);
}
}
// MITC4-MAP-004
TEST(InpDomainMapping,
PreservesProcedureLoadAndOutputRequestBoundariesForShells) {
const std::string with_no_ops = ReplaceOnce(
ShellDeck(), "*End Step\n",
"*Output, field\n*Node Output\nU, RF\n*Element Output\nS\n*End Step\n");
auto valid = MapText("mitc4-map-004-no-ops", with_no_ops);
ASSERT_TRUE(valid.HasValue());
ASSERT_EQ(valid.Value().Warnings().size(), 3U);
EXPECT_EQ(valid.Value().Warnings()[0].keyword, "OUTPUT");
EXPECT_EQ(valid.Value().Warnings()[1].keyword, "NODE OUTPUT");
EXPECT_EQ(valid.Value().Warnings()[2].keyword, "ELEMENT OUTPUT");
struct InvalidCase {
std::string name;
std::string deck;
std::string expected_code;
};
const std::string base = ShellDeck();
const std::vector<InvalidCase> cases{
{"second-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n",
"unsupported-multiple-step"},
{"nonlinear-step", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"),
"unsupported-nonlinear-geometry"},
{"other-procedure",
ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.",
"*Dynamic\n0.1, 1., 0.01, 1."),
"unsupported-keyword"},
{"distributed-load",
ReplaceOnce(base, "*Cload\nTipSecond, 6, 1.", "*Dload\nShellS4, P, 1."),
"unsupported-distributed-load"}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.name);
auto result = MapText("mitc4-map-004-" + test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.expected_code),
nullptr);
}
}
TEST(InpDomainMapping, RejectsUnsupportedAndInvalidPortfolio) {
struct InvalidCase {
std::string name;
std::string deck;
std::string expected_code;
fesa::FailureCategory category;
};
const std::string base = MinimalDeck();
const std::vector<InvalidCase> cases{
{"b31", ReplaceOnce(base, "type=B33", "type=B31"),
"unsupported-element-formulation", fesa::FailureCategory::kInput},
{"transform",
ReplaceOnce(base, "*End Instance\n", "1., 0., 0.\n*End Instance\n"),
"unsupported-instance-transform", fesa::FailureCategory::kInput},
{"nested-assembly",
ReplaceOnce(base, "*End Assembly\n",
"*Assembly, name=Nested\n*End Assembly\n*End Assembly\n"),
"unsupported-nested-assembly", fesa::FailureCategory::kInput},
{"multiple-step", base + "*Step\n*Static\n1., 1., 1., 1.\n*End Step\n",
"unsupported-multiple-step", fesa::FailureCategory::kInput},
{"late-part",
ReplaceOnce(base, "*End Assembly\n*Material",
"*End Assembly\n*Part, name=Late\n*End Part\n*Material"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"material-before-assembly",
ReplaceOnce(base, "*Assembly, name=Assembly",
"*Material, name=Early\n*Elastic\n50., 0.2\n*Assembly, "
"name=Assembly"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"material-after-model-boundary",
ReplaceOnce(base,
"*Material, name=Steel\n*Elastic\n100., "
"0.25\n*Boundary\nRoot, 1, 6",
"*Boundary\nRoot, 1, 6\n*Material, "
"name=Steel\n*Elastic\n100., 0.25"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"keyword-after-step", base + "*Preprint, echo=NO\n",
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"assembly-instance-after-set",
ReplaceOnce(base,
"*Instance, name=Beam-1, part=BeamPart\n*End "
"Instance\n*Nset, nset=Root, instance=Beam-1\n1",
"*Nset, nset=Root, instance=Beam-1\n1\n*Instance, "
"name=Beam-1, part=BeamPart\n*End Instance"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"element-before-node",
ReplaceOnce(
base,
"*Node\n1, 0., 0., 0.\n2, 1., 0., 0.\n*Element, type=B33\n1, 1, 2",
"*Element, type=B33\n1, 1, 2\n*Node\n1, 0., 0., 0.\n2, 1., 0., "
"0."),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"shear-before-section-context",
ReplaceOnce(
base, "*Beam General Section",
"*Transverse Shear Stiffness\n1., 2., 3.\n*Beam General Section"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"incomplete-part",
ReplaceOnce(base,
base.substr(base.find("*Part"),
base.find("*End Part") +
std::string{"*End Part\n"}.size() -
base.find("*Part")),
"*Part, name=BeamPart\n*End Part\n"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"empty-assembly",
ReplaceOnce(base,
base.substr(base.find("*Assembly"),
base.find("*End Assembly") +
std::string{"*End Assembly\n"}.size() -
base.find("*Assembly")),
"*Assembly, name=Assembly\n*End Assembly\n"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"cload-before-static",
ReplaceOnce(base, "*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.",
"*Cload\nTip, 2, -1.\n*Static\n0.1, 1., 0.01, 1."),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"static-after-boundary",
ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"),
"*Static\n0.1, 1., 0.01, 1.",
"*Boundary\nRoot, 1, 6\n*Static\n0.1, 1., 0.01, 1."),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"boundary-after-cload",
ReplaceOnce(ReplaceOnce(base, "*Boundary\nRoot, 1, 6\n*Step", "*Step"),
"Tip, 2, -1.\n*End Step",
"Tip, 2, -1.\n*Boundary\nRoot, 1, 6\n*End Step"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"cload-after-no-op",
ReplaceOnce(base, "*Cload", "*Restart, write, frequency=0\n*Cload"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"step-without-static",
ReplaceOnce(base,
"*Static\n0.1, 1., 0.01, 1.\n*Cload\nTip, 2, -1.\n*End Step",
"*End Step"),
"invalid-keyword-location", fesa::FailureCategory::kInput},
{"dependent-instance",
ReplaceOnce(base, "part=BeamPart", "part=BeamPart, dependent=YES"),
"unsupported-instance-mesh-semantics", fesa::FailureCategory::kInput},
{"coupled-section",
ReplaceOnce(base, "1., 1., 0., 1., 1.", "1., 1., 0.5, 1., 1."),
"unsupported-coupled-section", fesa::FailureCategory::kModel},
{"zero-length", ReplaceOnce(base, "2, 1., 0., 0.", "2, 0., 0., 0."),
"invalid-beam-length", fesa::FailureCategory::kModel},
{"parallel-guide", ReplaceOnce(base, "0., 1., 0.", "1., 0., 0."),
"invalid-beam-guide-vector", fesa::FailureCategory::kModel},
{"nonpositive-area",
ReplaceOnce(base, "1., 1., 0., 1., 1.", "0., 1., 0., 1., 1."),
"invalid-beam-property", fesa::FailureCategory::kModel},
{"nonpositive-derived-shear",
ReplaceOnce(base, "100., 0.25", "100., -1.25"), "invalid-beam-property",
fesa::FailureCategory::kModel},
{"nonfinite-elastic", ReplaceOnce(base, "100., 0.25", "inf, 0.25"),
"invalid-beam-property", fesa::FailureCategory::kModel},
{"nonfinite-section-property",
ReplaceOnce(base, "1., 1., 0., 1., 1.", "nan, 1., 0., 1., 1."),
"invalid-beam-property", fesa::FailureCategory::kModel},
{"nonfinite-guide", ReplaceOnce(base, "0., 1., 0.", "0., inf, 0."),
"invalid-beam-guide-vector", fesa::FailureCategory::kModel},
{"length-at-threshold",
ReplaceOnce(base, "2, 1., 0., 0.", "2, 1e-12, 0., 0."),
"invalid-beam-length", fesa::FailureCategory::kModel},
{"guide-at-threshold", ReplaceOnce(base, "0., 1., 0.", "1., 1e-12, 0."),
"invalid-beam-guide-vector", fesa::FailureCategory::kModel},
{"duplicate-elastic",
ReplaceOnce(base, "100., 0.25\n*Boundary",
"100., 0.25\n*Elastic\n100., 0.25\n*Boundary"),
"duplicate-entity", fesa::FailureCategory::kInput},
{"duplicate-node-label",
ReplaceOnce(base, "2, 1., 0., 0.", "1, 1., 0., 0."), "duplicate-entity",
fesa::FailureCategory::kInput},
{"dangling-connectivity", ReplaceOnce(base, "1, 1, 2", "1, 1, 9"),
"unresolved-reference", fesa::FailureCategory::kInput},
{"invalid-dof", ReplaceOnce(base, "Root, 1, 6", "Root, 1, 7"),
"invalid-dof", fesa::FailureCategory::kInput},
{"nonfinite-coordinate", ReplaceOnce(base, "1., 0., 0.", "nan, 0., 0."),
"invalid-numeric-value", fesa::FailureCategory::kInput},
{"malformed-node-arity", ReplaceOnce(base, "1, 0., 0., 0.", "1, 0., 0."),
"invalid-data-arity", fesa::FailureCategory::kInput},
{"nlgeom", ReplaceOnce(base, "nlgeom=NO", "nlgeom=YES"),
"unsupported-nonlinear-geometry", fesa::FailureCategory::kModel},
{"unknown-keyword",
ReplaceOnce(base, "*Assembly, name=Assembly",
"*Density\n1.\n*Assembly, name=Assembly"),
"unsupported-keyword", fesa::FailureCategory::kInput},
{"invalid-static-arity",
ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 0.01"),
"invalid-static-data", fesa::FailureCategory::kInput},
{"invalid-static-range",
ReplaceOnce(base, "0.1, 1., 0.01, 1.", "0.1, 1., 2., 1."),
"invalid-static-data", fesa::FailureCategory::kInput},
{"invalid-generate",
ReplaceOnce(base, "*Elset, elset=BeamSet\n1",
"*Elset, elset=BeamSet, generate\n1, 1, 0"),
"invalid-set-range", fesa::FailureCategory::kInput},
{"nonlanding-generate",
ReplaceOnce(base, "*Elset, elset=BeamSet\n1",
"*Elset, elset=BeamSet, generate\n1, 2, 2"),
"invalid-set-range", fesa::FailureCategory::kInput},
{"ambiguous-direct-label",
ReplaceOnce(
ReplaceOnce(base, "*End Instance\n*Nset, nset=Root",
"*End Instance\n*Instance, name=Beam-2, "
"part=BeamPart\n*End Instance\n*Nset, nset=Root"),
"Root, 1, 6", "1, 1, 6"),
"unresolved-reference", fesa::FailureCategory::kInput},
{"ambiguous-part-set",
ReplaceOnce(
ReplaceOnce(
ReplaceOnce(base, "*Elset, elset=BeamSet",
"*Nset, nset=Local\n1\n*Elset, elset=BeamSet"),
"*End Instance\n*Nset, nset=Root",
"*End Instance\n*Instance, name=Beam-2, part=BeamPart\n*End "
"Instance\n*Nset, nset=Root"),
"Root, 1, 6", "Local, 1, 6"),
"unresolved-reference", fesa::FailureCategory::kInput},
{"direct-set-conflict",
ReplaceOnce(base, "*Step, name=Load",
"*Boundary\n1, 1, 1, 2.\n*Step, name=Load"),
"conflicting-boundary-condition", fesa::FailureCategory::kInput},
{"dangling-boundary-target",
ReplaceOnce(base, "Root, 1, 6", "Missing, 1, 6"), "unresolved-reference",
fesa::FailureCategory::kInput},
{"conflicting-boundary",
ReplaceOnce(base, "*Step, name=Load",
"*Boundary\nRoot, 1, 1, 2.\n*Step, name=Load"),
"conflicting-boundary-condition", fesa::FailureCategory::kInput}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.name);
auto result = MapText("invalid-" + test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), test_case.category);
const auto* diagnostic =
FindDiagnostic(result.GetStatus(), test_case.expected_code);
ASSERT_NE(diagnostic, nullptr);
EXPECT_EQ(diagnostic->severity, fesa::Severity::kError);
EXPECT_FALSE(diagnostic->location.file.empty());
EXPECT_GT(diagnostic->location.line, 0U);
}
struct SameTokenAmbiguityCase {
std::string name;
std::string deck;
std::string expected_keyword;
std::size_t expected_line;
};
const std::string same_token_base = ReplaceOnce(
base, "*Elset, elset=BeamSet", "*Nset, nset=1\n2\n*Elset, elset=BeamSet");
const std::vector<SameTokenAmbiguityCase> same_token_cases{
{"boundary", ReplaceOnce(same_token_base, "Root, 1, 6", "1, 1, 6"),
"BOUNDARY", 27U},
{"cload", ReplaceOnce(same_token_base, "Tip, 2, -1.", "1, 2, -1."),
"CLOAD", 32U}};
for (const auto& test_case : same_token_cases) {
SCOPED_TRACE("same-token-" + test_case.name);
auto result = MapText("same-token-" + test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
const auto* diagnostic =
FindDiagnostic(result.GetStatus(), "unresolved-reference");
ASSERT_NE(diagnostic, nullptr);
EXPECT_EQ(diagnostic->severity, fesa::Severity::kError);
EXPECT_EQ(diagnostic->keyword, test_case.expected_keyword);
EXPECT_EQ(diagnostic->entity_identity, "1");
EXPECT_EQ(diagnostic->location.line, test_case.expected_line);
EXPECT_EQ(diagnostic->location.file.filename().string(),
"fesa-domain-mapper-same-token-" + test_case.name + ".inp");
}
}
TEST(InpDomainMapping, RejectsDloadWithoutDistributedLoadObject) {
const auto deck = ReplaceOnce(MinimalDeck(), "*Cload\nTip, 2, -1.\n",
"*Dload\nBeamSet, PY, -1.\n");
auto result = MapText("dload", deck);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
const auto* diagnostic =
FindDiagnostic(result.GetStatus(), "unsupported-keyword");
ASSERT_NE(diagnostic, nullptr);
EXPECT_EQ(diagnostic->keyword, "DLOAD");
}