Compare commits
12 Commits
b851575554
...
e7f0d53406
| Author | SHA1 | Date | |
|---|---|---|---|
| e7f0d53406 | |||
| fb1e4e9c77 | |||
| aa1ff76b80 | |||
| 154f8ef4af | |||
| 0f53ec48eb | |||
| 4401566287 | |||
| dab927a015 | |||
| abbc408aec | |||
| ceaf92f2a9 | |||
| 5a5af9c64b | |||
| 96f3f3230e | |||
| c7e64eb841 |
@@ -27,6 +27,10 @@ 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/semantic_mapper.cpp
|
||||||
|
src/fesa/model/domain.cpp
|
||||||
|
src/fesa/model/domain_builder.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(fesa_core
|
target_include_directories(fesa_core
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/source_location.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct DeckRecord final {
|
||||||
|
std::string keyword;
|
||||||
|
std::map<std::string, std::string, std::less<>> parameters;
|
||||||
|
std::vector<std::vector<std::string>> data;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedPart final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<DeckRecord> records;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedInstance final {
|
||||||
|
std::string name;
|
||||||
|
std::string part_name;
|
||||||
|
std::vector<std::vector<std::string>> transform_data;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedAssembly final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<ParsedInstance> instances;
|
||||||
|
std::vector<DeckRecord> records;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParsedDeck final {
|
||||||
|
std::vector<DeckRecord> global_records;
|
||||||
|
std::vector<ParsedPart> parts;
|
||||||
|
std::optional<ParsedAssembly> assembly;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/io/abaqus/deck_record.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ParseDeckResult final {
|
||||||
|
std::optional<ParsedDeck> deck;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ParseDeckResult parse_deck(
|
||||||
|
const std::filesystem::path& path);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -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,18 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
#include <fesa/model/entity_origin.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct BeamElement final {
|
||||||
|
ElementId id;
|
||||||
|
EntityOrigin origin;
|
||||||
|
std::array<NodeId, 2> nodes;
|
||||||
|
MaterialId material;
|
||||||
|
SectionId section;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/vec3.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
enum class ShearPropertySource { input, phase1_default };
|
||||||
|
|
||||||
|
struct BeamSection final {
|
||||||
|
SectionId id;
|
||||||
|
std::string name;
|
||||||
|
double area;
|
||||||
|
double iy;
|
||||||
|
double iz;
|
||||||
|
double torsion_j;
|
||||||
|
double shear_area_y;
|
||||||
|
double shear_area_z;
|
||||||
|
ShearPropertySource shear_source;
|
||||||
|
Vec3 orientation;
|
||||||
|
std::vector<std::array<double, 2>> recovery_points;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/model/beam_element.hpp>
|
||||||
|
#include <fesa/model/beam_section.hpp>
|
||||||
|
#include <fesa/model/entity_set.hpp>
|
||||||
|
#include <fesa/model/material.hpp>
|
||||||
|
#include <fesa/model/node.hpp>
|
||||||
|
#include <fesa/model/step_definition.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
class DomainBuilder;
|
||||||
|
|
||||||
|
class Domain final {
|
||||||
|
public:
|
||||||
|
Domain(const Domain&) = default;
|
||||||
|
Domain(Domain&&) noexcept = default;
|
||||||
|
Domain& operator=(const Domain&) = default;
|
||||||
|
Domain& operator=(Domain&&) noexcept = default;
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const Node> nodes() const noexcept {
|
||||||
|
return nodes_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const BeamElement> beam_elements() const noexcept {
|
||||||
|
return beam_elements_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const IsotropicElastic> materials() const noexcept {
|
||||||
|
return materials_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const BeamSection> sections() const noexcept {
|
||||||
|
return sections_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const NodeSet> node_sets() const noexcept {
|
||||||
|
return node_sets_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] std::span<const ElementSet> element_sets() const noexcept {
|
||||||
|
return element_sets_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const StepDefinition& step() const noexcept {
|
||||||
|
return step_;
|
||||||
|
}
|
||||||
|
|
||||||
|
[[nodiscard]] const Node& node(NodeId id) const;
|
||||||
|
[[nodiscard]] const Node& node(const EntityOrigin& origin) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class DomainBuilder;
|
||||||
|
|
||||||
|
using OriginKey = std::pair<std::string, std::int64_t>;
|
||||||
|
|
||||||
|
Domain(
|
||||||
|
std::vector<Node> nodes,
|
||||||
|
std::vector<BeamElement> beam_elements,
|
||||||
|
std::vector<IsotropicElastic> materials,
|
||||||
|
std::vector<BeamSection> sections,
|
||||||
|
std::vector<NodeSet> node_sets,
|
||||||
|
std::vector<ElementSet> element_sets,
|
||||||
|
StepDefinition step);
|
||||||
|
|
||||||
|
[[nodiscard]] static OriginKey origin_key(const EntityOrigin& origin);
|
||||||
|
|
||||||
|
std::vector<Node> nodes_;
|
||||||
|
std::vector<BeamElement> beam_elements_;
|
||||||
|
std::vector<IsotropicElastic> materials_;
|
||||||
|
std::vector<BeamSection> sections_;
|
||||||
|
std::vector<NodeSet> node_sets_;
|
||||||
|
std::vector<ElementSet> element_sets_;
|
||||||
|
StepDefinition step_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> node_indices_;
|
||||||
|
std::map<OriginKey, std::size_t> node_origin_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> beam_element_indices_;
|
||||||
|
std::map<OriginKey, std::size_t> beam_element_origin_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> material_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> section_indices_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct DomainBuildResult final {
|
||||||
|
std::optional<Domain> domain;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DomainBuilder final {
|
||||||
|
public:
|
||||||
|
void add_node(Node value);
|
||||||
|
void add_material(IsotropicElastic value);
|
||||||
|
void add_section(BeamSection value);
|
||||||
|
void add_beam_element(BeamElement value);
|
||||||
|
void add_node_set(NodeSet value);
|
||||||
|
void add_element_set(ElementSet value);
|
||||||
|
void set_step(StepDefinition value);
|
||||||
|
[[nodiscard]] DomainBuildResult build() &&;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Node> nodes_;
|
||||||
|
std::vector<BeamElement> beam_elements_;
|
||||||
|
std::vector<IsotropicElastic> materials_;
|
||||||
|
std::vector<BeamSection> sections_;
|
||||||
|
std::vector<NodeSet> node_sets_;
|
||||||
|
std::vector<ElementSet> element_sets_;
|
||||||
|
std::optional<StepDefinition> step_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct EntityOrigin final {
|
||||||
|
std::string part_name;
|
||||||
|
std::string instance_name;
|
||||||
|
std::int64_t local_label;
|
||||||
|
|
||||||
|
bool operator==(const EntityOrigin&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct NodeSet final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<NodeId> members;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ElementSet final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<ElementId> members;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/entity_id.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct NodeTag;
|
||||||
|
struct ElementTag;
|
||||||
|
struct MaterialTag;
|
||||||
|
struct SectionTag;
|
||||||
|
|
||||||
|
using NodeId = EntityId<NodeTag>;
|
||||||
|
using ElementId = EntityId<ElementTag>;
|
||||||
|
using MaterialId = EntityId<MaterialTag>;
|
||||||
|
using SectionId = EntityId<SectionTag>;
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct IsotropicElastic final {
|
||||||
|
MaterialId id;
|
||||||
|
std::string name;
|
||||||
|
double young;
|
||||||
|
double poisson;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <fesa/core/vec3.hpp>
|
||||||
|
#include <fesa/model/entity_origin.hpp>
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct Node final {
|
||||||
|
NodeId id;
|
||||||
|
EntityOrigin origin;
|
||||||
|
Vec3 position;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/model/ids.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct PrescribedDof final {
|
||||||
|
NodeId node;
|
||||||
|
std::uint8_t dof;
|
||||||
|
double value;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct NodalLoad final {
|
||||||
|
NodeId node;
|
||||||
|
std::array<double, 6> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct StepDefinition final {
|
||||||
|
std::string name;
|
||||||
|
std::vector<PrescribedDof> prescribed_dofs;
|
||||||
|
std::vector<NodalLoad> nodal_loads;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -5,22 +5,36 @@
|
|||||||
{
|
{
|
||||||
"step": 0,
|
"step": 0,
|
||||||
"name": "semantic-domain-and-origin-types",
|
"name": "semantic-domain-and-origin-types",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added dependency-free model value headers, shear-property provenance, immutable Domain queries, and ModelTypes/EntityOrigin tests.",
|
||||||
|
"started_at": "2026-07-30T16:08:54+0900",
|
||||||
|
"completed_at": "2026-07-30T16:38:04+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 1,
|
"step": 1,
|
||||||
"name": "domain-validation",
|
"name": "domain-validation",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added domain_builder.hpp, model Domain/DomainBuilder implementations, private dense/origin lookups, aggregate ID/origin/reference/numeric/geometry/orientation/BC validation, and DomainBuilder/DomainValidation tests.",
|
||||||
|
"started_at": "2026-07-30T16:38:05+0900",
|
||||||
|
"completed_at": "2026-07-30T17:08:06+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 2,
|
"step": 2,
|
||||||
"name": "abaqus-scoped-syntax-parser",
|
"name": "abaqus-scoped-syntax-parser",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added scoped Abaqus syntax records/parser, flat and Part/Assembly/Instance fixtures, source-aware parser diagnostics, and AbaqusParser/ScopedDeck tests; external preset AC passed 10/10 after child sandbox toolchain blockage.",
|
||||||
|
"started_at": "2026-07-30T17:08:07+0900",
|
||||||
|
"completed_at": "2026-07-30T18:00:47+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"step": 3,
|
"step": 3,
|
||||||
"name": "active-instance-domain-normalization",
|
"name": "active-instance-domain-normalization",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"summary": "Added public parser-to-Domain normalization for flat and single untransformed Instance decks, active Part selection, provenance, Phase 1 shear defaults, loads/BCs, and source-aware organization diagnostics; direct fallback verification passed after the Harness child quota failure.",
|
||||||
|
"started_at": "2026-07-30T18:03:41+0900",
|
||||||
|
"completed_at": "2026-07-30T18:24:34+0900"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"created_at": "2026-07-30T16:08:54+0900",
|
||||||
|
"completed_at": "2026-07-30T18:25:41+0900"
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"step": 3,
|
||||||
|
"name": "active-instance-domain-normalization",
|
||||||
|
"exitCode": 0,
|
||||||
|
"stdout": "Manual fallback completed after the Harness child quota failure recorded in commit 4401566. TDD red/green was observed for the public semantic mapper and the extra-Instance source diagnostic. MSVC v145 Debug build passed without new warnings; focused DeckToDomain/ActiveInstance CTest passed 2/2; full CTest passed 12/12; Harness pytest passed 20/20; git diff --check exited 0.",
|
||||||
|
"stderr": ""
|
||||||
|
}
|
||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "domain-and-input-skeleton",
|
"dir": "domain-and-input-skeleton",
|
||||||
"status": "pending"
|
"status": "completed",
|
||||||
|
"completed_at": "2026-07-30T18:25:41+0900"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dir": "fem-and-beam-kernel",
|
"dir": "fem-and-beam-kernel",
|
||||||
|
|||||||
@@ -0,0 +1,411 @@
|
|||||||
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <fstream>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
enum class Scope { global, part, assembly, instance };
|
||||||
|
|
||||||
|
struct KeywordLine final {
|
||||||
|
std::string keyword;
|
||||||
|
std::map<std::string, std::string, std::less<>> parameters;
|
||||||
|
SourceLocation source;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string_view trim(const std::string_view value) {
|
||||||
|
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||||
|
const std::size_t first = value.find_first_not_of(whitespace);
|
||||||
|
if (first == std::string_view::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const std::size_t last = value.find_last_not_of(whitespace);
|
||||||
|
return value.substr(first, last - first + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string uppercase_ascii(std::string value) {
|
||||||
|
std::ranges::transform(value, value.begin(), [](const char character) {
|
||||||
|
if (character >= 'a' && character <= 'z') {
|
||||||
|
return static_cast<char>(character - 'a' + 'A');
|
||||||
|
}
|
||||||
|
return character;
|
||||||
|
});
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split_fields(const std::string_view value) {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::size_t first = 0;
|
||||||
|
while (true) {
|
||||||
|
const std::size_t comma = value.find(',', first);
|
||||||
|
const std::string_view field = comma == std::string_view::npos
|
||||||
|
? value.substr(first)
|
||||||
|
: value.substr(first, comma - first);
|
||||||
|
fields.emplace_back(trim(field));
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = comma + 1;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult failure(
|
||||||
|
const DiagnosticStage stage,
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
std::optional<SourceLocation> source) {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
diagnostics.push_back({
|
||||||
|
stage,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::move(source),
|
||||||
|
});
|
||||||
|
return {std::nullopt, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult syntax_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
SourceLocation source) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::syntax,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::move(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_supported_record(const std::string_view keyword) {
|
||||||
|
constexpr std::array supported{
|
||||||
|
std::string_view{"BEAM GENERAL SECTION"},
|
||||||
|
std::string_view{"BOUNDARY"},
|
||||||
|
std::string_view{"CLOAD"},
|
||||||
|
std::string_view{"ELASTIC"},
|
||||||
|
std::string_view{"ELEMENT"},
|
||||||
|
std::string_view{"ELSET"},
|
||||||
|
std::string_view{"END STEP"},
|
||||||
|
std::string_view{"MATERIAL"},
|
||||||
|
std::string_view{"NODE"},
|
||||||
|
std::string_view{"NSET"},
|
||||||
|
std::string_view{"STATIC"},
|
||||||
|
std::string_view{"STEP"},
|
||||||
|
std::string_view{"TRANSVERSE SHEAR STIFFNESS"},
|
||||||
|
};
|
||||||
|
return std::ranges::find(supported, keyword) != supported.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<KeywordLine> parse_keyword_line(
|
||||||
|
const std::string_view line,
|
||||||
|
const SourceLocation& source,
|
||||||
|
ParseDeckResult& error) {
|
||||||
|
const std::vector<std::string> fields = split_fields(line.substr(1));
|
||||||
|
if (fields.empty() || fields[0].empty()) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.empty_keyword",
|
||||||
|
"Abaqus keyword name is empty.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
KeywordLine parsed{
|
||||||
|
uppercase_ascii(fields[0]),
|
||||||
|
{},
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
for (std::size_t index = 1; index < fields.size(); ++index) {
|
||||||
|
const std::string_view field = fields[index];
|
||||||
|
if (field.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t equals = field.find('=');
|
||||||
|
const std::string_view key_text =
|
||||||
|
trim(field.substr(0, equals));
|
||||||
|
const std::string_view value_text =
|
||||||
|
equals == std::string_view::npos
|
||||||
|
? std::string_view{}
|
||||||
|
: trim(field.substr(equals + 1));
|
||||||
|
if (key_text.empty()) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_parameter",
|
||||||
|
"Abaqus keyword parameter name is empty.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string key = uppercase_ascii(std::string{key_text});
|
||||||
|
if (!parsed.parameters.emplace(key, value_text).second) {
|
||||||
|
error = syntax_failure(
|
||||||
|
"abaqus.syntax.duplicate_parameter",
|
||||||
|
"Abaqus keyword parameter '" + key +
|
||||||
|
"' is specified more than once.",
|
||||||
|
source);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string* parameter(
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
const std::string_view name) {
|
||||||
|
const auto found = keyword.parameters.find(name);
|
||||||
|
return found == keyword.parameters.end() ? nullptr : &found->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseDeckResult missing_parameter(
|
||||||
|
const KeywordLine& keyword,
|
||||||
|
const std::string_view parameter_name) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.missing_parameter",
|
||||||
|
"Abaqus *" + keyword.keyword + " requires parameter " +
|
||||||
|
std::string{parameter_name} + ".",
|
||||||
|
keyword.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ParseDeckResult parse_deck(const std::filesystem::path& path) {
|
||||||
|
std::ifstream input{path, std::ios::binary};
|
||||||
|
if (!input) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.open_failed",
|
||||||
|
"Unable to open Abaqus input file.",
|
||||||
|
SourceLocation{path, 0U, 0U});
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedDeck deck;
|
||||||
|
Scope scope = Scope::global;
|
||||||
|
std::optional<ParsedPart> current_part;
|
||||||
|
std::optional<ParsedAssembly> current_assembly;
|
||||||
|
std::optional<ParsedInstance> current_instance;
|
||||||
|
DeckRecord* current_record = nullptr;
|
||||||
|
|
||||||
|
std::string line;
|
||||||
|
std::size_t line_number = 0;
|
||||||
|
while (std::getline(input, line)) {
|
||||||
|
++line_number;
|
||||||
|
if (line_number == 1U && line.starts_with("\xEF\xBB\xBF")) {
|
||||||
|
line.erase(0, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t first_nonspace =
|
||||||
|
line.find_first_not_of(" \t\f\v\r");
|
||||||
|
if (first_nonspace == std::string::npos) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::string_view content = trim(line);
|
||||||
|
if (content.starts_with("**")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.starts_with('*')) {
|
||||||
|
const std::vector<std::string> fields = split_fields(content);
|
||||||
|
if (scope == Scope::instance) {
|
||||||
|
current_instance->transform_data.push_back(fields);
|
||||||
|
} else if (current_record != nullptr) {
|
||||||
|
current_record->data.push_back(fields);
|
||||||
|
} else {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.data_without_keyword",
|
||||||
|
"Abaqus data line has no preceding keyword record.",
|
||||||
|
SourceLocation{path, line_number, first_nonspace + 1U});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SourceLocation source{
|
||||||
|
path,
|
||||||
|
line_number,
|
||||||
|
first_nonspace + 1U,
|
||||||
|
};
|
||||||
|
ParseDeckResult keyword_error;
|
||||||
|
std::optional<KeywordLine> parsed =
|
||||||
|
parse_keyword_line(content, source, keyword_error);
|
||||||
|
if (!parsed.has_value()) {
|
||||||
|
return keyword_error;
|
||||||
|
}
|
||||||
|
KeywordLine& keyword = *parsed;
|
||||||
|
current_record = nullptr;
|
||||||
|
|
||||||
|
if (keyword.keyword == "PART") {
|
||||||
|
if (scope != Scope::global) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_part_scope",
|
||||||
|
"*PART is only valid in global input scope.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
current_part = ParsedPart{*name, {}, source};
|
||||||
|
scope = Scope::part;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END PART") {
|
||||||
|
if (scope != Scope::part || !current_part.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_part",
|
||||||
|
"*END PART does not match an open *PART.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
deck.parts.push_back(std::move(*current_part));
|
||||||
|
current_part.reset();
|
||||||
|
scope = Scope::global;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "ASSEMBLY") {
|
||||||
|
if (scope != Scope::global) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_assembly_scope",
|
||||||
|
"*ASSEMBLY is only valid in global input scope.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
if (deck.assembly.has_value() ||
|
||||||
|
current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.multiple_assemblies",
|
||||||
|
"ParsedDeck can preserve only one *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
current_assembly = ParsedAssembly{*name, {}, {}, source};
|
||||||
|
scope = Scope::assembly;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END ASSEMBLY") {
|
||||||
|
if (scope != Scope::assembly ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_assembly",
|
||||||
|
"*END ASSEMBLY does not match an open *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
deck.assembly = std::move(*current_assembly);
|
||||||
|
current_assembly.reset();
|
||||||
|
scope = Scope::global;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "INSTANCE") {
|
||||||
|
if (scope != Scope::assembly ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.invalid_instance_scope",
|
||||||
|
"*INSTANCE is only valid in an open *ASSEMBLY.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
const std::string* name = parameter(keyword, "NAME");
|
||||||
|
if (name == nullptr || name->empty()) {
|
||||||
|
return missing_parameter(keyword, "NAME");
|
||||||
|
}
|
||||||
|
const std::string* part_name = parameter(keyword, "PART");
|
||||||
|
if (part_name == nullptr || part_name->empty()) {
|
||||||
|
return missing_parameter(keyword, "PART");
|
||||||
|
}
|
||||||
|
current_instance =
|
||||||
|
ParsedInstance{*name, *part_name, {}, source};
|
||||||
|
scope = Scope::instance;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keyword.keyword == "END INSTANCE") {
|
||||||
|
if (scope != Scope::instance ||
|
||||||
|
!current_instance.has_value() ||
|
||||||
|
!current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unexpected_end_instance",
|
||||||
|
"*END INSTANCE does not match an open *INSTANCE.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
current_assembly->instances.push_back(
|
||||||
|
std::move(*current_instance));
|
||||||
|
current_instance.reset();
|
||||||
|
scope = Scope::assembly;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!is_supported_record(keyword.keyword)) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.unsupported_keyword",
|
||||||
|
"Unsupported Abaqus keyword *" + keyword.keyword + ".",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
|
||||||
|
DeckRecord next_record{
|
||||||
|
std::move(keyword.keyword),
|
||||||
|
std::move(keyword.parameters),
|
||||||
|
{},
|
||||||
|
source,
|
||||||
|
};
|
||||||
|
switch (scope) {
|
||||||
|
case Scope::global:
|
||||||
|
deck.global_records.push_back(std::move(next_record));
|
||||||
|
current_record = &deck.global_records.back();
|
||||||
|
break;
|
||||||
|
case Scope::part:
|
||||||
|
current_part->records.push_back(std::move(next_record));
|
||||||
|
current_record = ¤t_part->records.back();
|
||||||
|
break;
|
||||||
|
case Scope::assembly:
|
||||||
|
current_assembly->records.push_back(
|
||||||
|
std::move(next_record));
|
||||||
|
current_record = ¤t_assembly->records.back();
|
||||||
|
break;
|
||||||
|
case Scope::instance:
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.instance_local_keyword",
|
||||||
|
"Keyword records inside *INSTANCE are unsupported.",
|
||||||
|
source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"abaqus.io.read_failed",
|
||||||
|
"Failed while reading Abaqus input file.",
|
||||||
|
SourceLocation{path, line_number, 0U});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_instance.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_instance",
|
||||||
|
"Abaqus *INSTANCE is not closed by *END INSTANCE.",
|
||||||
|
current_instance->source);
|
||||||
|
}
|
||||||
|
if (current_part.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_part",
|
||||||
|
"Abaqus *PART is not closed by *END PART.",
|
||||||
|
current_part->source);
|
||||||
|
}
|
||||||
|
if (current_assembly.has_value()) {
|
||||||
|
return syntax_failure(
|
||||||
|
"abaqus.syntax.unclosed_assembly",
|
||||||
|
"Abaqus *ASSEMBLY is not closed by *END ASSEMBLY.",
|
||||||
|
current_assembly->source);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {std::move(deck), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,688 @@
|
|||||||
|
#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) {
|
||||||
|
const SourceLocation& source =
|
||||||
|
active_assembly_->instances.size() > 1U
|
||||||
|
? active_assembly_->instances[1].source
|
||||||
|
: active_assembly_->source;
|
||||||
|
add_error(
|
||||||
|
"abaqus.semantic.instance_count",
|
||||||
|
"Phase 1 requires exactly one Instance.",
|
||||||
|
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
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
Domain::Domain(
|
||||||
|
std::vector<Node> nodes,
|
||||||
|
std::vector<BeamElement> beam_elements,
|
||||||
|
std::vector<IsotropicElastic> materials,
|
||||||
|
std::vector<BeamSection> sections,
|
||||||
|
std::vector<NodeSet> node_sets,
|
||||||
|
std::vector<ElementSet> element_sets,
|
||||||
|
StepDefinition step)
|
||||||
|
: nodes_{std::move(nodes)},
|
||||||
|
beam_elements_{std::move(beam_elements)},
|
||||||
|
materials_{std::move(materials)},
|
||||||
|
sections_{std::move(sections)},
|
||||||
|
node_sets_{std::move(node_sets)},
|
||||||
|
element_sets_{std::move(element_sets)},
|
||||||
|
step_{std::move(step)} {
|
||||||
|
for (std::size_t index = 0; index < nodes_.size(); ++index) {
|
||||||
|
node_indices_.emplace(nodes_[index].id.value(), index);
|
||||||
|
node_origin_indices_.emplace(origin_key(nodes_[index].origin), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < beam_elements_.size(); ++index) {
|
||||||
|
beam_element_indices_.emplace(
|
||||||
|
beam_elements_[index].id.value(), index);
|
||||||
|
beam_element_origin_indices_.emplace(
|
||||||
|
origin_key(beam_elements_[index].origin), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < materials_.size(); ++index) {
|
||||||
|
material_indices_.emplace(materials_[index].id.value(), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < sections_.size(); ++index) {
|
||||||
|
section_indices_.emplace(sections_[index].id.value(), index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& Domain::node(const NodeId id) const {
|
||||||
|
const auto found = node_indices_.find(id.value());
|
||||||
|
if (found == node_indices_.end()) {
|
||||||
|
throw std::out_of_range{"Node ID is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return nodes_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& Domain::node(const EntityOrigin& origin) const {
|
||||||
|
const auto found = node_origin_indices_.find(origin_key(origin));
|
||||||
|
if (found == node_origin_indices_.end()) {
|
||||||
|
throw std::out_of_range{
|
||||||
|
"Node origin is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return nodes_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
Domain::OriginKey Domain::origin_key(const EntityOrigin& origin) {
|
||||||
|
return {origin.instance_name, origin.local_label};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using IndexLookup = std::unordered_map<std::int64_t, std::size_t>;
|
||||||
|
using OriginKey = std::pair<std::string, std::int64_t>;
|
||||||
|
|
||||||
|
void add_error(
|
||||||
|
std::vector<Diagnostic>& diagnostics,
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
diagnostics.push_back({
|
||||||
|
DiagnosticStage::model,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginKey origin_key(const EntityOrigin& origin) {
|
||||||
|
return {origin.instance_name, origin.local_label};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entity, class IdAccessor>
|
||||||
|
IndexLookup collect_ids(
|
||||||
|
const std::vector<Entity>& entities,
|
||||||
|
IdAccessor id_of,
|
||||||
|
const std::string_view duplicate_code,
|
||||||
|
const std::string_view entity_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
IndexLookup indices;
|
||||||
|
for (std::size_t index = 0; index < entities.size(); ++index) {
|
||||||
|
const std::int64_t id = id_of(entities[index]).value();
|
||||||
|
if (!indices.emplace(id, index).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
std::string{duplicate_code},
|
||||||
|
"Duplicate " + std::string{entity_name} +
|
||||||
|
" internal ID " + std::to_string(id) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entity>
|
||||||
|
void collect_duplicate_origins(
|
||||||
|
const std::vector<Entity>& entities,
|
||||||
|
const std::string_view duplicate_code,
|
||||||
|
const std::string_view entity_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::map<OriginKey, std::size_t> origins;
|
||||||
|
for (std::size_t index = 0; index < entities.size(); ++index) {
|
||||||
|
const bool inserted =
|
||||||
|
origins.emplace(origin_key(entities[index].origin), index).second;
|
||||||
|
if (!inserted) {
|
||||||
|
const EntityOrigin& origin = entities[index].origin;
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
std::string{duplicate_code},
|
||||||
|
"Duplicate " + std::string{entity_name} + " origin (" +
|
||||||
|
origin.instance_name + ", " +
|
||||||
|
std::to_string(origin.local_label) + ").");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_id(const IndexLookup& indices, const std::int64_t id) {
|
||||||
|
return indices.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_finite(const std::array<double, 2>& value) {
|
||||||
|
return std::isfinite(value[0]) && std::isfinite(value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_finite(const std::array<double, 6>& value) {
|
||||||
|
return std::ranges::all_of(
|
||||||
|
value, [](const double component) {
|
||||||
|
return std::isfinite(component);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_section_property(
|
||||||
|
const BeamSection& section,
|
||||||
|
const double value,
|
||||||
|
const std::string_view property_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) + " has a "
|
||||||
|
"nonfinite " +
|
||||||
|
std::string{property_name} + ".");
|
||||||
|
} else if (value <= 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_section",
|
||||||
|
"Section " + std::to_string(section.id.value()) + " requires " +
|
||||||
|
std::string{property_name} + " > 0.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double length(const Vec3 value) {
|
||||||
|
return std::hypot(value.x, value.y, value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_parallel(const Vec3 first, const Vec3 second) {
|
||||||
|
const double first_length = length(first);
|
||||||
|
const double second_length = length(second);
|
||||||
|
if (first_length == 0.0 || second_length == 0.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 first_unit{
|
||||||
|
first.x / first_length,
|
||||||
|
first.y / first_length,
|
||||||
|
first.z / first_length,
|
||||||
|
};
|
||||||
|
const Vec3 second_unit{
|
||||||
|
second.x / second_length,
|
||||||
|
second.y / second_length,
|
||||||
|
second.z / second_length,
|
||||||
|
};
|
||||||
|
const Vec3 cross{
|
||||||
|
first_unit.y * second_unit.z -
|
||||||
|
first_unit.z * second_unit.y,
|
||||||
|
first_unit.z * second_unit.x -
|
||||||
|
first_unit.x * second_unit.z,
|
||||||
|
first_unit.x * second_unit.y -
|
||||||
|
first_unit.y * second_unit.x,
|
||||||
|
};
|
||||||
|
return length(cross) <=
|
||||||
|
64.0 * std::numeric_limits<double>::epsilon();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DomainBuilder::add_node(Node value) {
|
||||||
|
nodes_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_material(IsotropicElastic value) {
|
||||||
|
materials_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_section(BeamSection value) {
|
||||||
|
sections_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_beam_element(BeamElement value) {
|
||||||
|
beam_elements_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_node_set(NodeSet value) {
|
||||||
|
node_sets_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_element_set(ElementSet value) {
|
||||||
|
element_sets_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::set_step(StepDefinition value) {
|
||||||
|
step_ = std::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
DomainBuildResult DomainBuilder::build() && {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
|
||||||
|
const IndexLookup node_indices = collect_ids(
|
||||||
|
nodes_,
|
||||||
|
[](const Node& node) { return node.id; },
|
||||||
|
"model.duplicate_node_id",
|
||||||
|
"node",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup material_indices = collect_ids(
|
||||||
|
materials_,
|
||||||
|
[](const IsotropicElastic& material) { return material.id; },
|
||||||
|
"model.duplicate_material_id",
|
||||||
|
"material",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup section_indices = collect_ids(
|
||||||
|
sections_,
|
||||||
|
[](const BeamSection& section) { return section.id; },
|
||||||
|
"model.duplicate_section_id",
|
||||||
|
"section",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup element_indices = collect_ids(
|
||||||
|
beam_elements_,
|
||||||
|
[](const BeamElement& element) { return element.id; },
|
||||||
|
"model.duplicate_element_id",
|
||||||
|
"Beam element",
|
||||||
|
diagnostics);
|
||||||
|
|
||||||
|
collect_duplicate_origins(
|
||||||
|
nodes_,
|
||||||
|
"model.duplicate_node_origin",
|
||||||
|
"node",
|
||||||
|
diagnostics);
|
||||||
|
collect_duplicate_origins(
|
||||||
|
beam_elements_,
|
||||||
|
"model.duplicate_element_origin",
|
||||||
|
"Beam element",
|
||||||
|
diagnostics);
|
||||||
|
|
||||||
|
for (const Node& node : nodes_) {
|
||||||
|
if (!is_finite(node.position)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Node " + std::to_string(node.id.value()) +
|
||||||
|
" has a nonfinite coordinate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const IsotropicElastic& material : materials_) {
|
||||||
|
if (!std::isfinite(material.young)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" has a nonfinite Young's modulus.");
|
||||||
|
} else if (material.young <= 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_material",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" requires E > 0.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::isfinite(material.poisson)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" has a nonfinite Poisson ratio.");
|
||||||
|
} else if (
|
||||||
|
material.poisson <= -1.0 || material.poisson >= 0.5) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_material",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" requires -1 < nu < 0.5.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const BeamSection& section : sections_) {
|
||||||
|
validate_section_property(
|
||||||
|
section, section.area, "A", diagnostics);
|
||||||
|
validate_section_property(section, section.iy, "Iy", diagnostics);
|
||||||
|
validate_section_property(section, section.iz, "Iz", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.torsion_j, "J", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.shear_area_y, "Asy", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.shear_area_z, "Asz", diagnostics);
|
||||||
|
|
||||||
|
if (!is_finite(section.orientation)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a nonfinite orientation.");
|
||||||
|
} else if (length(section.orientation) == 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_orientation",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a zero orientation vector.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& recovery_point : section.recovery_points) {
|
||||||
|
if (!is_finite(recovery_point)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a nonfinite recovery point.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const BeamElement& element : beam_elements_) {
|
||||||
|
const bool has_first_node =
|
||||||
|
has_id(node_indices, element.nodes[0].value());
|
||||||
|
const bool has_second_node =
|
||||||
|
has_id(node_indices, element.nodes[1].value());
|
||||||
|
if (!has_first_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(element.nodes[0].value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_second_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(element.nodes[1].value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_id(material_indices, element.material.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_material_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing material " +
|
||||||
|
std::to_string(element.material.value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_id(section_indices, element.section.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_section_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing section " +
|
||||||
|
std::to_string(element.section.value()) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_first_node || !has_second_node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& first = nodes_[node_indices.at(element.nodes[0].value())];
|
||||||
|
const Node& second =
|
||||||
|
nodes_[node_indices.at(element.nodes[1].value())];
|
||||||
|
if (!is_finite(first.position) || !is_finite(second.position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 axis{
|
||||||
|
second.position.x - first.position.x,
|
||||||
|
second.position.y - first.position.y,
|
||||||
|
second.position.z - first.position.z,
|
||||||
|
};
|
||||||
|
if (length(axis) == 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.zero_length_element",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" has zero length.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto section_found =
|
||||||
|
section_indices.find(element.section.value());
|
||||||
|
if (section_found == section_indices.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Vec3 orientation =
|
||||||
|
sections_[section_found->second].orientation;
|
||||||
|
if (is_finite(orientation) && length(orientation) > 0.0 &&
|
||||||
|
is_parallel(axis, orientation)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_orientation",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" has an orientation parallel to its axis.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const NodeSet& node_set : node_sets_) {
|
||||||
|
for (const NodeId member : node_set.members) {
|
||||||
|
if (!has_id(node_indices, member.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Node set " + node_set.name +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(member.value()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ElementSet& element_set : element_sets_) {
|
||||||
|
for (const ElementId member : element_set.members) {
|
||||||
|
if (!has_id(element_indices, member.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_element_reference",
|
||||||
|
"Element set " + element_set.name +
|
||||||
|
" references missing element " +
|
||||||
|
std::to_string(member.value()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!step_.has_value()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_step",
|
||||||
|
"The Domain requires one linear static step.");
|
||||||
|
} else {
|
||||||
|
std::map<std::pair<std::int64_t, std::uint8_t>, double>
|
||||||
|
prescribed_values;
|
||||||
|
for (const PrescribedDof& prescribed : step_->prescribed_dofs) {
|
||||||
|
if (!has_id(node_indices, prescribed.node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Boundary condition references missing node " +
|
||||||
|
std::to_string(prescribed.node.value()) + ".");
|
||||||
|
}
|
||||||
|
if (prescribed.dof < 1 || prescribed.dof > 6) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_dof",
|
||||||
|
"Boundary condition DOF must be in [1, 6].");
|
||||||
|
}
|
||||||
|
if (!std::isfinite(prescribed.value)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Boundary condition has a nonfinite value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto key =
|
||||||
|
std::pair{prescribed.node.value(), prescribed.dof};
|
||||||
|
if (!prescribed_values.emplace(key, prescribed.value).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.conflicting_boundary_condition",
|
||||||
|
"A node DOF has more than one prescribed value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const NodalLoad& load : step_->nodal_loads) {
|
||||||
|
if (!has_id(node_indices, load.node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Concentrated load references missing node " +
|
||||||
|
std::to_string(load.node.value()) + ".");
|
||||||
|
}
|
||||||
|
if (!is_finite(load.values)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Concentrated load has a nonfinite component.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diagnostics.empty()) {
|
||||||
|
return {std::nullopt, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Domain{
|
||||||
|
std::move(nodes_),
|
||||||
|
std::move(beam_elements_),
|
||||||
|
std::move(materials_),
|
||||||
|
std::move(sections_),
|
||||||
|
std::move(node_sets_),
|
||||||
|
std::move(element_sets_),
|
||||||
|
std::move(*step_),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -75,3 +75,98 @@ add_test(
|
|||||||
NAME CoreValueTypes
|
NAME CoreValueTypes
|
||||||
COMMAND "$<TARGET_FILE:fesa_core_value_tests>"
|
COMMAND "$<TARGET_FILE:fesa_core_value_tests>"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_model_value_tests
|
||||||
|
unit/model/domain_builder_test.cpp
|
||||||
|
unit/model/entity_origin_test.cpp
|
||||||
|
unit/model/model_types_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(fesa_model_value_tests PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(fesa_model_value_tests PRIVATE /W4 /permissive- /EHsc)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_model_value_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ModelTypes
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=ModelTypes.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME EntityOrigin
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=EntityOrigin.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DomainBuilder
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=DomainBuilder.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DomainValidation
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=*DomainValidation*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(fesa_abaqus_parser_tests
|
||||||
|
unit/io/abaqus/parser_test.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(fesa_abaqus_parser_tests PRIVATE cxx_std_20)
|
||||||
|
target_compile_options(fesa_abaqus_parser_tests PRIVATE /W4 /permissive- /EHsc)
|
||||||
|
target_compile_definitions(
|
||||||
|
fesa_abaqus_parser_tests
|
||||||
|
PRIVATE
|
||||||
|
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(fesa_abaqus_parser_tests
|
||||||
|
PRIVATE
|
||||||
|
fesa_core
|
||||||
|
GTest::gtest_main
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME AbaqusParser
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--gtest_filter=AbaqusParser.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ScopedDeck
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_abaqus_parser_tests>"
|
||||||
|
--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.*
|
||||||
|
)
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
** Minimal flat/orphan-mesh cantilever syntax fixture.
|
||||||
|
|
||||||
|
*nOdE
|
||||||
|
1, 0.0, 0.0, 0.0
|
||||||
|
** Comments and blank lines do not terminate the current data record.
|
||||||
|
|
||||||
|
2, 1.0, 0.0, 0.0,
|
||||||
|
*eLeMeNt, TYPE=B31, elset=Beam
|
||||||
|
1, 1, 2
|
||||||
|
*NSET, NSET=Fixed
|
||||||
|
1
|
||||||
|
*ELSET, ELSET=Beam
|
||||||
|
1
|
||||||
|
*MATERIAL, NAME=Steel
|
||||||
|
*ELASTIC
|
||||||
|
210000.0, 0.3
|
||||||
|
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0
|
||||||
|
0.0, 1.0, 0.0
|
||||||
|
*BOUNDARY
|
||||||
|
Fixed, 1, 6
|
||||||
|
*STEP, NAME=Load
|
||||||
|
*STATIC
|
||||||
|
*CLOAD
|
||||||
|
2, 2, -1.0
|
||||||
|
*END STEP
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
** Minimal scoped cantilever syntax fixture.
|
||||||
|
*PART, NAME=BeamPart
|
||||||
|
*NODE
|
||||||
|
1, 0.0, 0.0, 0.0
|
||||||
|
2, 1.0, 0.0, 0.0
|
||||||
|
*ELEMENT, TYPE=B31, ELSET=Beam
|
||||||
|
1, 1, 2
|
||||||
|
*NSET, NSET=Fixed
|
||||||
|
1
|
||||||
|
*NSET, NSET=Tip
|
||||||
|
2
|
||||||
|
*ELSET, ELSET=Beam
|
||||||
|
1
|
||||||
|
*BEAM GENERAL SECTION, SECTION=GENERAL, ELSET=Beam, MATERIAL=Steel
|
||||||
|
1.0, 1.0, 1.0, 1.0, 1.0
|
||||||
|
0.0, 1.0, 0.0
|
||||||
|
*END PART
|
||||||
|
|
||||||
|
*ASSEMBLY, NAME=RootAssembly
|
||||||
|
*INSTANCE, NAME=Beam-1, PART=BeamPart
|
||||||
|
*END INSTANCE
|
||||||
|
*NSET, NSET=Fixed, INSTANCE=Beam-1
|
||||||
|
1
|
||||||
|
*NSET, NSET=Tip, INSTANCE=Beam-1
|
||||||
|
2
|
||||||
|
*END ASSEMBLY
|
||||||
|
|
||||||
|
*MATERIAL, NAME=Steel
|
||||||
|
*ELASTIC
|
||||||
|
210000.0, 0.3
|
||||||
|
*STEP, NAME=Load
|
||||||
|
*STATIC
|
||||||
|
*BOUNDARY
|
||||||
|
Fixed, 1, 6
|
||||||
|
*CLOAD
|
||||||
|
Tip, 2, -1.0
|
||||||
|
*END STEP
|
||||||
@@ -0,0 +1,268 @@
|
|||||||
|
#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());
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,235 @@
|
|||||||
|
#include <fesa/io/abaqus/parser.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fesa::DeckRecord& record(
|
||||||
|
const std::vector<fesa::DeckRecord>& records,
|
||||||
|
const std::string_view keyword) {
|
||||||
|
const auto found = std::ranges::find(
|
||||||
|
records, keyword, &fesa::DeckRecord::keyword);
|
||||||
|
if (found == records.end()) {
|
||||||
|
throw std::runtime_error{"Expected deck record was not parsed."};
|
||||||
|
}
|
||||||
|
return *found;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, ParsesCaseInsensitiveKeywordsCommentsAndCommaFields) {
|
||||||
|
const auto path = fixture_path("minimal_cantilever.inp");
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
EXPECT_TRUE(result.deck->parts.empty());
|
||||||
|
EXPECT_FALSE(result.deck->assembly.has_value());
|
||||||
|
|
||||||
|
const auto& nodes = record(result.deck->global_records, "NODE");
|
||||||
|
ASSERT_EQ(nodes.data.size(), 2U);
|
||||||
|
EXPECT_EQ(nodes.data[0], (std::vector<std::string>{
|
||||||
|
"1", "0.0", "0.0", "0.0"}));
|
||||||
|
EXPECT_EQ(nodes.data[1], (std::vector<std::string>{
|
||||||
|
"2", "1.0", "0.0", "0.0", ""}));
|
||||||
|
EXPECT_EQ(nodes.source.file, path);
|
||||||
|
EXPECT_EQ(nodes.source.line, 3U);
|
||||||
|
EXPECT_EQ(nodes.source.column, 1U);
|
||||||
|
|
||||||
|
const auto& element = record(result.deck->global_records, "ELEMENT");
|
||||||
|
EXPECT_EQ(element.parameters.at("TYPE"), "B31");
|
||||||
|
EXPECT_EQ(element.parameters.at("ELSET"), "Beam");
|
||||||
|
EXPECT_EQ(element.source.line, 8U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, PreservesUtf8ParameterValues) {
|
||||||
|
const std::string utf8_name{
|
||||||
|
"\xEB\xB9\x94\xEB\xB6\x80\xED\x92\x88"};
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-utf8.inp",
|
||||||
|
"*MATERIAL, NAME=" + utf8_name + "\n"
|
||||||
|
"*ELASTIC\n"
|
||||||
|
"210000.0, 0.3\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
const auto& material = record(result.deck->global_records, "MATERIAL");
|
||||||
|
EXPECT_EQ(material.parameters.at("NAME"), utf8_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(AbaqusParser, RejectsUnsupportedKeywordsInsteadOfIgnoringThem) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-unsupported.inp",
|
||||||
|
"** The error source must identify the keyword line.\n"
|
||||||
|
"*INCLUDE, INPUT=other.inp\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.deck.has_value());
|
||||||
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.diagnostics[0].stage, fesa::DiagnosticStage::syntax);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.diagnostics[0].code, "abaqus.unsupported_keyword");
|
||||||
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->file, input.path());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->line, 2U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, PreservesPartAssemblyAndInstanceScopes) {
|
||||||
|
const auto path =
|
||||||
|
fixture_path("minimal_part_instance_cantilever.inp");
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(path);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.deck->parts.size(), 1U);
|
||||||
|
EXPECT_EQ(result.deck->parts[0].name, "BeamPart");
|
||||||
|
EXPECT_EQ(result.deck->parts[0].source.file, path);
|
||||||
|
EXPECT_EQ(result.deck->parts[0].source.line, 2U);
|
||||||
|
EXPECT_EQ(record(result.deck->parts[0].records, "NODE").source.line, 3U);
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
||||||
|
EXPECT_EQ(result.deck->assembly->name, "RootAssembly");
|
||||||
|
EXPECT_EQ(result.deck->assembly->source.line, 19U);
|
||||||
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].name, "Beam-1");
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].part_name, "BeamPart");
|
||||||
|
EXPECT_EQ(result.deck->assembly->instances[0].source.line, 20U);
|
||||||
|
EXPECT_TRUE(
|
||||||
|
result.deck->assembly->instances[0].transform_data.empty());
|
||||||
|
EXPECT_EQ(
|
||||||
|
record(result.deck->assembly->records, "NSET")
|
||||||
|
.parameters.at("INSTANCE"),
|
||||||
|
"Beam-1");
|
||||||
|
|
||||||
|
EXPECT_NE(
|
||||||
|
std::ranges::find(
|
||||||
|
result.deck->global_records,
|
||||||
|
"MATERIAL",
|
||||||
|
&fesa::DeckRecord::keyword),
|
||||||
|
result.deck->global_records.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, PreservesInstanceTransformDataWithoutApplyingIt) {
|
||||||
|
const TemporaryDeck input{
|
||||||
|
"fesa-parser-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"
|
||||||
|
"0.0, 0.0, 1.0, 90.0\n"
|
||||||
|
"*END INSTANCE\n"
|
||||||
|
"*END ASSEMBLY\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.deck.has_value());
|
||||||
|
ASSERT_TRUE(result.deck->assembly.has_value());
|
||||||
|
ASSERT_EQ(result.deck->assembly->instances.size(), 1U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.deck->assembly->instances[0].transform_data,
|
||||||
|
(std::vector<std::vector<std::string>>{
|
||||||
|
{"1.0", "2.0", "3.0"},
|
||||||
|
{"0.0", "0.0", "1.0", "90.0"}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ScopedDeck, ReportsUnexpectedAndUnclosedScopeTerminators) {
|
||||||
|
struct Case final {
|
||||||
|
std::string_view name;
|
||||||
|
std::string_view contents;
|
||||||
|
std::string_view code;
|
||||||
|
std::size_t line;
|
||||||
|
};
|
||||||
|
const Case cases[]{
|
||||||
|
{
|
||||||
|
"fesa-parser-unexpected-end-part.inp",
|
||||||
|
"*END PART\n",
|
||||||
|
"abaqus.syntax.unexpected_end_part",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unclosed-part.inp",
|
||||||
|
"*PART, NAME=BeamPart\n"
|
||||||
|
"*NODE\n"
|
||||||
|
"1, 0.0, 0.0, 0.0\n",
|
||||||
|
"abaqus.syntax.unclosed_part",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unexpected-end-instance.inp",
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n"
|
||||||
|
"*END INSTANCE\n",
|
||||||
|
"abaqus.syntax.unexpected_end_instance",
|
||||||
|
2U,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fesa-parser-unclosed-assembly.inp",
|
||||||
|
"*ASSEMBLY, NAME=RootAssembly\n",
|
||||||
|
"abaqus.syntax.unclosed_assembly",
|
||||||
|
1U,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const auto& test_case : cases) {
|
||||||
|
const TemporaryDeck input{test_case.name, test_case.contents};
|
||||||
|
|
||||||
|
const auto result = fesa::parse_deck(input.path());
|
||||||
|
|
||||||
|
SCOPED_TRACE(test_case.name);
|
||||||
|
EXPECT_FALSE(result.deck.has_value());
|
||||||
|
ASSERT_EQ(result.diagnostics.size(), 1U);
|
||||||
|
EXPECT_EQ(result.diagnostics[0].code, test_case.code);
|
||||||
|
ASSERT_TRUE(result.diagnostics[0].source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics[0].source->line, test_case.line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,572 @@
|
|||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <limits>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::Node first_node() {
|
||||||
|
return {
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
fesa::Vec3{0.0, 0.0, 0.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Node second_node() {
|
||||||
|
return {
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{2.0, 0.0, 0.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::IsotropicElastic valid_material() {
|
||||||
|
return {
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamSection valid_section() {
|
||||||
|
return {
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"General",
|
||||||
|
0.04,
|
||||||
|
1.2e-4,
|
||||||
|
1.4e-4,
|
||||||
|
2.0e-4,
|
||||||
|
0.03,
|
||||||
|
0.031,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{{-0.1, 0.0}, {0.1, 0.0}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamElement valid_element() {
|
||||||
|
return {
|
||||||
|
fesa::ElementId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::StepDefinition valid_step() {
|
||||||
|
return {
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{0}, 1, 0.0}},
|
||||||
|
{{fesa::NodeId{1}, {0.0, -100.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::DomainBuilder make_builder(
|
||||||
|
fesa::IsotropicElastic material,
|
||||||
|
fesa::BeamSection section,
|
||||||
|
fesa::BeamElement element,
|
||||||
|
fesa::StepDefinition step) {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
builder.add_node(first_node());
|
||||||
|
builder.add_node(second_node());
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
builder.add_section(std::move(section));
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
builder.add_node_set({"Fixed", {fesa::NodeId{0}}});
|
||||||
|
builder.add_element_set({"Beam", {fesa::ElementId{0}}});
|
||||||
|
builder.set_step(std::move(step));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::DomainBuilder make_valid_builder() {
|
||||||
|
return make_builder(
|
||||||
|
valid_material(), valid_section(), valid_element(), valid_step());
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t diagnostic_count(
|
||||||
|
const fesa::DomainBuildResult& result,
|
||||||
|
const std::string_view code) {
|
||||||
|
return static_cast<std::size_t>(std::ranges::count_if(
|
||||||
|
result.diagnostics,
|
||||||
|
[code](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainBuilder, BuildsImmutableDomainAndDenseNodeLookups) {
|
||||||
|
auto result = std::move(make_valid_builder()).build();
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
|
||||||
|
const fesa::Domain& domain = *result.domain;
|
||||||
|
ASSERT_EQ(domain.nodes().size(), 2);
|
||||||
|
ASSERT_EQ(domain.beam_elements().size(), 1);
|
||||||
|
ASSERT_EQ(domain.materials().size(), 1);
|
||||||
|
ASSERT_EQ(domain.sections().size(), 1);
|
||||||
|
ASSERT_EQ(domain.node_sets().size(), 1);
|
||||||
|
ASSERT_EQ(domain.element_sets().size(), 1);
|
||||||
|
EXPECT_EQ(domain.step().name, "Load");
|
||||||
|
EXPECT_EQ(&domain.node(fesa::NodeId{1}), &domain.nodes()[1]);
|
||||||
|
EXPECT_EQ(
|
||||||
|
&domain.node(fesa::EntityOrigin{"BeamPart", "Beam-1", 2}),
|
||||||
|
&domain.nodes()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateInternalId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
|
||||||
|
fesa::Vec3{3.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateMaterialId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto material = valid_material();
|
||||||
|
material.name = "Duplicate";
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_material_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateSectionId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto section = valid_section();
|
||||||
|
section.name = "Duplicate";
|
||||||
|
builder.add_section(std::move(section));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_section_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateElementId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto element = valid_element();
|
||||||
|
element.origin.local_label = 2;
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateOriginWithinEntityKind) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{2},
|
||||||
|
fesa::EntityOrigin{"OtherPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{3.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateElementOriginWithinEntityKind) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto element = valid_element();
|
||||||
|
element.id = fesa::ElementId{1};
|
||||||
|
element.origin.part_name = "OtherPart";
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsMissingReferencesAcrossSemanticEntities) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
{fesa::NodeId{90}, fesa::NodeId{91}},
|
||||||
|
fesa::MaterialId{92},
|
||||||
|
fesa::SectionId{93},
|
||||||
|
});
|
||||||
|
builder.add_node_set({"MissingNodes", {fesa::NodeId{94}}});
|
||||||
|
builder.add_element_set({"MissingElements", {fesa::ElementId{95}}});
|
||||||
|
builder.set_step({
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{96}, 1, 0.0}},
|
||||||
|
{{fesa::NodeId{97}, {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_node_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_element_reference"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonpositiveYoungsModulus) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = 0.0;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsPoissonRatioOutsideOpenPhysicalRange) {
|
||||||
|
for (const double poisson : {-1.0, 0.5}) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.poisson = poisson;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value()) << "poisson=" << poisson;
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"))
|
||||||
|
<< "poisson=" << poisson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvalidSectionProperty final {
|
||||||
|
const char* name;
|
||||||
|
double fesa::BeamSection::*member;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DomainValidationInvalidSection
|
||||||
|
: public testing::TestWithParam<InvalidSectionProperty> {};
|
||||||
|
|
||||||
|
TEST_P(DomainValidationInvalidSection, RejectsNonpositiveProperty) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.*(GetParam().member) = 0.0;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(
|
||||||
|
SectionProperties,
|
||||||
|
DomainValidationInvalidSection,
|
||||||
|
testing::Values(
|
||||||
|
InvalidSectionProperty{"Area", &fesa::BeamSection::area},
|
||||||
|
InvalidSectionProperty{"Iy", &fesa::BeamSection::iy},
|
||||||
|
InvalidSectionProperty{"Iz", &fesa::BeamSection::iz},
|
||||||
|
InvalidSectionProperty{"TorsionJ", &fesa::BeamSection::torsion_j},
|
||||||
|
InvalidSectionProperty{
|
||||||
|
"ShearAreaY", &fesa::BeamSection::shear_area_y},
|
||||||
|
InvalidSectionProperty{
|
||||||
|
"ShearAreaZ", &fesa::BeamSection::shear_area_z}),
|
||||||
|
[](const testing::TestParamInfo<InvalidSectionProperty>& info) {
|
||||||
|
return info.param.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteNodeCoordinate) {
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{2},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
|
||||||
|
fesa::Vec3{nan, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteMaterialConstant) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfinitePoissonRatio) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.poisson = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteSectionProperty) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteSectionOrientation) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.orientation.y = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteRecoveryPoint) {
|
||||||
|
auto section = valid_section();
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
section.recovery_points = {{nan, 0.0}};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfinitePrescribedValue) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.prescribed_dofs[0].value =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteLoadComponent) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.nodal_loads[0].values[0] =
|
||||||
|
std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentMaterialPropertyDiagnostics) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
material.poisson = 0.5;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentSectionPropertyDiagnostics) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = -1.0;
|
||||||
|
section.iy = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsNonfiniteStepValuesIndependently) {
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
const double infinity = std::numeric_limits<double>::infinity();
|
||||||
|
auto step = fesa::StepDefinition{
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{0}, 1, infinity}},
|
||||||
|
{{fesa::NodeId{1}, {nan, 0.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_EQ(diagnostic_count(result, "model.nonfinite_value"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsZeroLengthElement) {
|
||||||
|
auto element = valid_element();
|
||||||
|
element.nodes[1] = fesa::NodeId{0};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
element,
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.zero_length_element"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsZeroAndElementParallelOrientation) {
|
||||||
|
for (const fesa::Vec3 orientation :
|
||||||
|
{fesa::Vec3{0.0, 0.0, 0.0}, fesa::Vec3{1.0, 0.0, 0.0}}) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.orientation = orientation;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsElementWithoutMaterialAndSectionAssignments) {
|
||||||
|
auto element = valid_element();
|
||||||
|
element.material = fesa::MaterialId{8};
|
||||||
|
element.section = fesa::SectionId{9};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
element,
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsConflictingBoundaryConditions) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.prescribed_dofs.push_back({fesa::NodeId{0}, 1, 0.25});
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_diagnostic(result, "model.conflicting_boundary_condition"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentDiagnosticsWithoutEarlyExit) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = -1.0;
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = -1.0;
|
||||||
|
section.orientation = fesa::Vec3{1.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
static_assert(std::is_same_v<
|
||||||
|
decltype(std::declval<const fesa::Domain&>().nodes()),
|
||||||
|
std::span<const fesa::Node>>);
|
||||||
|
static_assert(std::is_same_v<
|
||||||
|
decltype(std::declval<const fesa::Domain&>().node(
|
||||||
|
std::declval<fesa::NodeId>())),
|
||||||
|
const fesa::Node&>);
|
||||||
|
|
||||||
|
TEST(EntityOrigin, UsesEmptyScopeNamesForFlatMeshEntities) {
|
||||||
|
const fesa::EntityOrigin origin{"", "", 17};
|
||||||
|
|
||||||
|
EXPECT_TRUE(origin.part_name.empty());
|
||||||
|
EXPECT_TRUE(origin.instance_name.empty());
|
||||||
|
EXPECT_EQ(origin.local_label, 17);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(EntityOrigin, FindsHierarchicalNodeByCompositeOrigin) {
|
||||||
|
const fesa::Node node{
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 101},
|
||||||
|
fesa::Vec3{1.0, 2.0, 3.0},
|
||||||
|
};
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
builder.add_node(node);
|
||||||
|
builder.set_step(fesa::StepDefinition{"Load", {}, {}});
|
||||||
|
auto result = std::move(builder).build();
|
||||||
|
ASSERT_TRUE(result.domain.has_value());
|
||||||
|
const fesa::Domain& domain = *result.domain;
|
||||||
|
|
||||||
|
const fesa::EntityOrigin lookup{"BeamPart", "Beam-1", 101};
|
||||||
|
const fesa::Node& found_by_origin = domain.node(lookup);
|
||||||
|
const fesa::Node& found_by_id = domain.node(fesa::NodeId{0});
|
||||||
|
|
||||||
|
EXPECT_EQ(&found_by_origin, &domain.nodes().front());
|
||||||
|
EXPECT_EQ(&found_by_id, &domain.nodes().front());
|
||||||
|
EXPECT_EQ(found_by_origin.origin.part_name, "BeamPart");
|
||||||
|
EXPECT_EQ(found_by_origin.origin.instance_name, "Beam-1");
|
||||||
|
EXPECT_EQ(found_by_origin.origin.local_label, 101);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
#include <fesa/model/beam_element.hpp>
|
||||||
|
#include <fesa/model/beam_section.hpp>
|
||||||
|
#include <fesa/model/entity_set.hpp>
|
||||||
|
#include <fesa/model/material.hpp>
|
||||||
|
#include <fesa/model/node.hpp>
|
||||||
|
#include <fesa/model/step_definition.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
TEST(ModelTypes, PreservesNodeAndBeamElementValues) {
|
||||||
|
const fesa::Node first_node{
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 101},
|
||||||
|
fesa::Vec3{1.0, 2.0, 3.0},
|
||||||
|
};
|
||||||
|
const fesa::BeamElement element{
|
||||||
|
fesa::ElementId{4},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 201},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{2},
|
||||||
|
fesa::SectionId{3},
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_EQ(first_node.id.value(), 0);
|
||||||
|
EXPECT_EQ(first_node.origin.part_name, "BeamPart");
|
||||||
|
EXPECT_EQ(first_node.origin.instance_name, "Beam-1");
|
||||||
|
EXPECT_EQ(first_node.origin.local_label, 101);
|
||||||
|
EXPECT_DOUBLE_EQ(first_node.position.x, 1.0);
|
||||||
|
EXPECT_DOUBLE_EQ(first_node.position.y, 2.0);
|
||||||
|
EXPECT_DOUBLE_EQ(first_node.position.z, 3.0);
|
||||||
|
|
||||||
|
EXPECT_EQ(element.id.value(), 4);
|
||||||
|
EXPECT_EQ(element.origin.local_label, 201);
|
||||||
|
EXPECT_EQ(element.nodes[0].value(), 0);
|
||||||
|
EXPECT_EQ(element.nodes[1].value(), 1);
|
||||||
|
EXPECT_EQ(element.material.value(), 2);
|
||||||
|
EXPECT_EQ(element.section.value(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ModelTypes, PreservesMaterialAndBeamSectionValues) {
|
||||||
|
const fesa::IsotropicElastic material{
|
||||||
|
fesa::MaterialId{2},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
};
|
||||||
|
const fesa::BeamSection section{
|
||||||
|
fesa::SectionId{3},
|
||||||
|
"General",
|
||||||
|
0.04,
|
||||||
|
1.2e-4,
|
||||||
|
1.4e-4,
|
||||||
|
2.0e-4,
|
||||||
|
0.03,
|
||||||
|
0.031,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{{-0.1, 0.0}, {0.1, 0.0}},
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_EQ(material.id.value(), 2);
|
||||||
|
EXPECT_EQ(material.name, "Steel");
|
||||||
|
EXPECT_DOUBLE_EQ(material.young, 210.0e9);
|
||||||
|
EXPECT_DOUBLE_EQ(material.poisson, 0.3);
|
||||||
|
|
||||||
|
EXPECT_EQ(section.id.value(), 3);
|
||||||
|
EXPECT_EQ(section.name, "General");
|
||||||
|
EXPECT_DOUBLE_EQ(section.area, 0.04);
|
||||||
|
EXPECT_DOUBLE_EQ(section.iy, 1.2e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(section.iz, 1.4e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(section.torsion_j, 2.0e-4);
|
||||||
|
EXPECT_DOUBLE_EQ(section.shear_area_y, 0.03);
|
||||||
|
EXPECT_DOUBLE_EQ(section.shear_area_z, 0.031);
|
||||||
|
EXPECT_EQ(section.shear_source, fesa::ShearPropertySource::input);
|
||||||
|
EXPECT_DOUBLE_EQ(section.orientation.y, 1.0);
|
||||||
|
ASSERT_EQ(section.recovery_points.size(), 2);
|
||||||
|
EXPECT_DOUBLE_EQ(section.recovery_points[0][0], -0.1);
|
||||||
|
EXPECT_DOUBLE_EQ(section.recovery_points[1][0], 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ModelTypes, PreservesSetsAndLinearStaticStepValues) {
|
||||||
|
const fesa::NodeSet node_set{
|
||||||
|
"Fixed",
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
};
|
||||||
|
const fesa::ElementSet element_set{
|
||||||
|
"Beam",
|
||||||
|
{fesa::ElementId{4}},
|
||||||
|
};
|
||||||
|
const fesa::StepDefinition step{
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{0}, 6, 0.25}},
|
||||||
|
{{
|
||||||
|
fesa::NodeId{1},
|
||||||
|
{10.0, 20.0, 30.0, 40.0, 50.0, 60.0},
|
||||||
|
}},
|
||||||
|
};
|
||||||
|
|
||||||
|
EXPECT_EQ(node_set.name, "Fixed");
|
||||||
|
ASSERT_EQ(node_set.members.size(), 2);
|
||||||
|
EXPECT_EQ(node_set.members[1].value(), 1);
|
||||||
|
EXPECT_EQ(element_set.name, "Beam");
|
||||||
|
ASSERT_EQ(element_set.members.size(), 1);
|
||||||
|
EXPECT_EQ(element_set.members[0].value(), 4);
|
||||||
|
|
||||||
|
EXPECT_EQ(step.name, "Load");
|
||||||
|
ASSERT_EQ(step.prescribed_dofs.size(), 1);
|
||||||
|
EXPECT_EQ(step.prescribed_dofs[0].node.value(), 0);
|
||||||
|
EXPECT_EQ(step.prescribed_dofs[0].dof, 6);
|
||||||
|
EXPECT_DOUBLE_EQ(step.prescribed_dofs[0].value, 0.25);
|
||||||
|
ASSERT_EQ(step.nodal_loads.size(), 1);
|
||||||
|
EXPECT_EQ(step.nodal_loads[0].node.value(), 1);
|
||||||
|
EXPECT_DOUBLE_EQ(step.nodal_loads[0].values[5], 60.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user