feat(cpp-object-oriented-modular-refactoring): step 11 - source-target-resolver
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef FESA_CORE_ASCII_H_
|
||||||
|
#define FESA_CORE_ASCII_H_
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Converts one ASCII uppercase byte to lowercase.
|
||||||
|
/// @return The lowercase ASCII byte, or the input byte when it is not A-Z.
|
||||||
|
char AsciiLower(char value) noexcept;
|
||||||
|
|
||||||
|
/// @brief Compares two byte strings with ASCII-only case folding.
|
||||||
|
/// @return True when the strings have equal length and equal ASCII-folded
|
||||||
|
/// bytes.
|
||||||
|
bool AsciiCaseInsensitiveEquals(std::string_view lhs,
|
||||||
|
std::string_view rhs) noexcept;
|
||||||
|
|
||||||
|
/// @brief Parses a complete positive base-10 source label.
|
||||||
|
/// @return The positive label or an input failure for malformed, nonpositive,
|
||||||
|
/// or out-of-range text.
|
||||||
|
Result<std::int64_t> ParsePositiveSourceLabel(std::string_view text);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_CORE_ASCII_H_
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
#ifndef FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
|
||||||
|
#define FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/source_identity.h"
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/model/model_types.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
class Domain;
|
||||||
|
|
||||||
|
/// @brief Selects the independent source node or element namespace.
|
||||||
|
enum class SourceEntityKind { kNode, kElement };
|
||||||
|
|
||||||
|
/// @brief Maps one direct label or named-target membership to stable identity.
|
||||||
|
/// @note An empty target_name denotes a direct source-label entry.
|
||||||
|
struct SourceTargetIndexEntry {
|
||||||
|
SourceEntityKind entity_kind;
|
||||||
|
std::string instance_name;
|
||||||
|
std::string target_name;
|
||||||
|
SourceEntityId source_id;
|
||||||
|
EntityIndex entity_index;
|
||||||
|
std::size_t declaration_order;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Owns immutable compact source-target lookup entries.
|
||||||
|
class SourceTargetIndex {
|
||||||
|
public:
|
||||||
|
/// @brief Takes ownership of compact entries from a validated model
|
||||||
|
/// candidate.
|
||||||
|
explicit SourceTargetIndex(std::vector<SourceTargetIndexEntry> entries);
|
||||||
|
|
||||||
|
/// @brief Builds compact entries from an immutable semantic Domain.
|
||||||
|
/// @return An owning index that preserves Domain declaration order.
|
||||||
|
static SourceTargetIndex FromDomain(const Domain& domain);
|
||||||
|
|
||||||
|
/// @brief Returns owned entries without exposing mutable index state.
|
||||||
|
const std::vector<SourceTargetIndexEntry>& Entries() const noexcept;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<SourceTargetIndexEntry> entries_;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Describes one source target lookup.
|
||||||
|
struct SourceTargetQuery {
|
||||||
|
SourceEntityKind entity_kind;
|
||||||
|
std::string instance_name;
|
||||||
|
std::string target_name_or_label;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Preserves both external source identity and stable internal index.
|
||||||
|
struct ResolvedSourceTarget {
|
||||||
|
SourceEntityId source_id;
|
||||||
|
EntityIndex entity_index;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Resolves source labels and named targets without owning model state.
|
||||||
|
/// @note The referenced SourceTargetIndex must outlive this resolver.
|
||||||
|
class SourceTargetResolver {
|
||||||
|
public:
|
||||||
|
/// @brief Creates a non-owning resolver over an immutable index.
|
||||||
|
/// @param index Index whose lifetime must exceed the resolver lifetime.
|
||||||
|
explicit SourceTargetResolver(const SourceTargetIndex& index) noexcept;
|
||||||
|
|
||||||
|
/// @brief Resolves one query in stable declaration order.
|
||||||
|
/// @return Stable source targets, or a deterministic input diagnostic for an
|
||||||
|
/// invalid, missing, duplicate, or ambiguous target.
|
||||||
|
Result<std::vector<ResolvedSourceTarget>> Resolve(
|
||||||
|
const SourceTargetQuery& query) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const SourceTargetIndex* index_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_MODEL_SOURCE_TARGET_RESOLVER_H_
|
||||||
@@ -10,6 +10,7 @@ add_library(
|
|||||||
assembly/sparse_assembler.cpp
|
assembly/sparse_assembler.cpp
|
||||||
build_info.cpp
|
build_info.cpp
|
||||||
constraints/essential_constraints.cpp
|
constraints/essential_constraints.cpp
|
||||||
|
core/ascii.cpp
|
||||||
core/diagnostic.cpp
|
core/diagnostic.cpp
|
||||||
core/status.cpp
|
core/status.cpp
|
||||||
elements/euler_beam_3d.cpp
|
elements/euler_beam_3d.cpp
|
||||||
@@ -24,6 +25,7 @@ add_library(
|
|||||||
math/vector.cpp
|
math/vector.cpp
|
||||||
model/domain.cpp
|
model/domain.cpp
|
||||||
model/shell_geometry.cpp
|
model/shell_geometry.cpp
|
||||||
|
model/source_target_resolver.cpp
|
||||||
results/result_recovery.cpp
|
results/result_recovery.cpp
|
||||||
solvers/linear/mkl_pardiso_solver.cpp
|
solvers/linear/mkl_pardiso_solver.cpp
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
#include "fesa/assembly/load_assembler.h"
|
#include "fesa/assembly/load_assembler.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <charconv>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdint>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <system_error>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "fesa/constraints/essential_constraints.h"
|
#include "fesa/constraints/essential_constraints.h"
|
||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -27,28 +25,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location,
|
|||||||
{{Severity::kError, code, location, keyword, identity, message}});
|
{{Severity::kError, code, location, keyword, identity, message}});
|
||||||
}
|
}
|
||||||
|
|
||||||
char AsciiLower(const char value) {
|
|
||||||
if (value >= 'A' && value <= 'Z') {
|
|
||||||
return static_cast<char>(value + ('a' - 'A'));
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EqualName(const std::string& left, const std::string& right) {
|
|
||||||
return left.size() == right.size() &&
|
|
||||||
std::equal(left.begin(), left.end(), right.begin(),
|
|
||||||
[](const char left_value, const char right_value) {
|
|
||||||
return AsciiLower(left_value) == AsciiLower(right_value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
|
|
||||||
const char* const first = text.data();
|
|
||||||
const char* const last = first + text.size();
|
|
||||||
const auto parsed = std::from_chars(first, last, value);
|
|
||||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// @brief Checks that equation-space indices preserve stable full-DOF order.
|
/// @brief Checks that equation-space indices preserve stable full-DOF order.
|
||||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||||
return std::adjacent_find(
|
return std::adjacent_find(
|
||||||
@@ -122,36 +98,21 @@ Status ValidateDofOrder(const DofManager& dofs,
|
|||||||
return Status::Ok();
|
return Status::Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain,
|
Result<std::vector<EntityIndex>> ResolveTarget(
|
||||||
|
const SourceTargetResolver& resolver, const Domain& domain,
|
||||||
const NodalLoad& load) {
|
const NodalLoad& load) {
|
||||||
std::vector<const NodeSet*> matching_sets;
|
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
|
||||||
for (const auto& set : domain.NodeSets()) {
|
if (!resolved.HasValue()) {
|
||||||
if (EqualName(set.name, load.target)) {
|
|
||||||
matching_sets.push_back(&set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<EntityIndex> matching_nodes;
|
|
||||||
std::int64_t label = 0;
|
|
||||||
if (TryPositiveInteger(load.target, label)) {
|
|
||||||
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
|
|
||||||
if (domain.Nodes()[index].source_id.source_label == label) {
|
|
||||||
matching_nodes.push_back(static_cast<EntityIndex>(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matching_sets.size() > 1U || matching_nodes.size() > 1U ||
|
|
||||||
(!matching_sets.empty() && !matching_nodes.empty())) {
|
|
||||||
return Result<std::vector<EntityIndex>>::Failure(
|
return Result<std::vector<EntityIndex>>::Failure(
|
||||||
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
|
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
|
||||||
"The load target must resolve unambiguously to one node or "
|
"The load target must resolve unambiguously to one node or "
|
||||||
"one expanded node set."));
|
"one expanded node set."));
|
||||||
}
|
}
|
||||||
if (!matching_sets.empty()) {
|
std::vector<EntityIndex> nodes;
|
||||||
const auto& nodes = matching_sets.front()->node_indices;
|
nodes.reserve(resolved.Value().size());
|
||||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||||
for (const EntityIndex node : nodes) {
|
for (const auto& target : resolved.Value()) {
|
||||||
|
const EntityIndex node = target.entity_index;
|
||||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
||||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
"invalid-load-target", load.location, "CLOAD", load.target,
|
||||||
@@ -159,15 +120,9 @@ Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain,
|
|||||||
"identities."));
|
"identities."));
|
||||||
}
|
}
|
||||||
seen[node] = 1U;
|
seen[node] = 1U;
|
||||||
|
nodes.push_back(node);
|
||||||
}
|
}
|
||||||
return Result<std::vector<EntityIndex>>::Success(nodes);
|
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
|
||||||
}
|
|
||||||
if (!matching_nodes.empty()) {
|
|
||||||
return Result<std::vector<EntityIndex>>::Success(std::move(matching_nodes));
|
|
||||||
}
|
|
||||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
|
||||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
|
||||||
"The load target must resolve to one semantic node or node set."));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Status ValidateFiniteVector(const Vector& values,
|
Status ValidateFiniteVector(const Vector& values,
|
||||||
@@ -285,6 +240,8 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
|
|||||||
"invalid-load-order", model.Step().location, "CLOAD", model.Step().name,
|
"invalid-load-order", model.Step().location, "CLOAD", model.Step().name,
|
||||||
"The active load view must include every sole-step load once."));
|
"The active load view must include every sole-step load once."));
|
||||||
}
|
}
|
||||||
|
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
|
||||||
|
const SourceTargetResolver target_resolver{target_index};
|
||||||
|
|
||||||
Vector full_load{expected_full_count};
|
Vector full_load{expected_full_count};
|
||||||
// Active load indices are required to be the original source order; this
|
// Active load indices are required to be the original source order; this
|
||||||
@@ -311,7 +268,7 @@ Result<Vector> LoadAssembler::AssembleFullNodalLoad(const AnalysisModel& model,
|
|||||||
load.target, "A nodal load magnitude must be finite."));
|
load.target, "A nodal load magnitude must be finite."));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto target = ResolveTarget(domain, load);
|
auto target = ResolveTarget(target_resolver, domain, load);
|
||||||
if (!target.HasValue()) {
|
if (!target.HasValue()) {
|
||||||
return Result<Vector>::Failure(target.GetStatus());
|
return Result<Vector>::Failure(target.GetStatus());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#include "fesa/core/ascii.h"
|
||||||
|
|
||||||
|
#include <charconv>
|
||||||
|
#include <string>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
char AsciiLower(const char value) noexcept {
|
||||||
|
if (value >= 'A' && value <= 'Z') {
|
||||||
|
return static_cast<char>(value + ('a' - 'A'));
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AsciiCaseInsensitiveEquals(const std::string_view lhs,
|
||||||
|
const std::string_view rhs) noexcept {
|
||||||
|
if (lhs.size() != rhs.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < lhs.size(); ++index) {
|
||||||
|
if (AsciiLower(lhs[index]) != AsciiLower(rhs[index])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::int64_t> ParsePositiveSourceLabel(const std::string_view text) {
|
||||||
|
std::int64_t value = 0;
|
||||||
|
const char* const storage = text.empty() ? "" : text.data();
|
||||||
|
const char* const first = storage;
|
||||||
|
const char* const last = storage + text.size();
|
||||||
|
const auto parsed = std::from_chars(first, last, value, 10);
|
||||||
|
if (parsed.ec == std::errc{} && parsed.ptr == last && value > 0) {
|
||||||
|
return Result<std::int64_t>::Success(value);
|
||||||
|
}
|
||||||
|
const std::string identity{text};
|
||||||
|
return Result<std::int64_t>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kInput,
|
||||||
|
{{Severity::kError,
|
||||||
|
"invalid-source-label",
|
||||||
|
{{}, 0U},
|
||||||
|
"SOURCE_LABEL",
|
||||||
|
identity,
|
||||||
|
"A source label must be a complete positive base-10 integer."}}));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -1,56 +1,29 @@
|
|||||||
#include "fesa/fem/dof_manager.h"
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <charconv>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
|
||||||
#include <system_error>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::size_t kDofsPerNode = 6U;
|
constexpr std::size_t kDofsPerNode = 6U;
|
||||||
|
|
||||||
char AsciiLower(char value) {
|
|
||||||
if (value >= 'A' && value <= 'Z') {
|
|
||||||
return static_cast<char>(value + ('a' - 'A'));
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EqualName(const std::string& left, const std::string& right) {
|
|
||||||
return left.size() == right.size() &&
|
|
||||||
std::equal(left.begin(), left.end(), right.begin(),
|
|
||||||
[](char left_value, char right_value) {
|
|
||||||
return AsciiLower(left_value) == AsciiLower(right_value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
|
|
||||||
const char* const first = text.data();
|
|
||||||
const char* const last = first + text.size();
|
|
||||||
const auto parsed = std::from_chars(first, last, value);
|
|
||||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<EntityIndex> ExpandBoundaryTarget(
|
std::vector<EntityIndex> ExpandBoundaryTarget(
|
||||||
const Domain& domain, const BoundaryCondition& boundary) {
|
const SourceTargetResolver& resolver, const BoundaryCondition& boundary) {
|
||||||
for (const auto& set : domain.NodeSets()) {
|
auto resolved =
|
||||||
if (EqualName(set.name, boundary.target)) {
|
resolver.Resolve({SourceEntityKind::kNode, "", boundary.target});
|
||||||
return set.node_indices;
|
if (!resolved.HasValue()) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::int64_t source_label = 0;
|
|
||||||
if (TryPositiveInteger(boundary.target, source_label)) {
|
|
||||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
|
||||||
if (domain.Nodes()[node].source_id.source_label == source_label) {
|
|
||||||
return {static_cast<EntityIndex>(node)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {};
|
return {};
|
||||||
|
}
|
||||||
|
std::vector<EntityIndex> indices;
|
||||||
|
indices.reserve(resolved.Value().size());
|
||||||
|
for (const auto& target : resolved.Value()) {
|
||||||
|
indices.push_back(target.entity_index);
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <std::size_t scatter_size>
|
template <std::size_t scatter_size>
|
||||||
@@ -94,12 +67,14 @@ SparsePattern BuildSparsePattern(
|
|||||||
|
|
||||||
Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||||
const Domain& domain = model.GetDomain();
|
const Domain& domain = model.GetDomain();
|
||||||
|
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
|
||||||
|
const SourceTargetResolver target_resolver{target_index};
|
||||||
const std::size_t full_count = domain.Nodes().size() * kDofsPerNode;
|
const std::size_t full_count = domain.Nodes().size() * kDofsPerNode;
|
||||||
|
|
||||||
std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
|
std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
|
||||||
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
|
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
|
||||||
const auto& boundary = model.Step().boundaries.at(boundary_index);
|
const auto& boundary = model.Step().boundaries.at(boundary_index);
|
||||||
const auto target = ExpandBoundaryTarget(domain, boundary);
|
const auto target = ExpandBoundaryTarget(target_resolver, boundary);
|
||||||
for (const EntityIndex node : target) {
|
for (const EntityIndex node : target) {
|
||||||
for (int component = boundary.first_dof; component <= boundary.last_dof;
|
for (int component = boundary.first_dof; component <= boundary.last_dof;
|
||||||
++component) {
|
++component) {
|
||||||
|
|||||||
@@ -14,26 +14,14 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/ascii.h"
|
||||||
#include "fesa/math/vector3.h"
|
#include "fesa/math/vector3.h"
|
||||||
#include "fesa/model/shell_geometry.h"
|
#include "fesa/model/shell_geometry.h"
|
||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::string UppercaseAscii(std::string value) {
|
|
||||||
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
|
|
||||||
if (character >= 'a' && character <= 'z') {
|
|
||||||
return static_cast<char>(character - 'a' + 'A');
|
|
||||||
}
|
|
||||||
return character;
|
|
||||||
});
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EqualName(const std::string& left, const std::string& right) {
|
|
||||||
return UppercaseAscii(left) == UppercaseAscii(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
|
std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
|
||||||
while (!fields.empty() && fields.back().empty()) {
|
while (!fields.empty() && fields.back().empty()) {
|
||||||
fields.pop_back();
|
fields.pop_back();
|
||||||
@@ -254,20 +242,6 @@ class MappingContext {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) const {
|
|
||||||
if (text.empty()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
char* end = nullptr;
|
|
||||||
errno = 0;
|
|
||||||
const long long parsed = std::strtoll(text.c_str(), &end, 10);
|
|
||||||
if (errno == ERANGE || end != text.c_str() + text.size() || parsed <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
value = static_cast<std::int64_t>(parsed);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ParseDouble(const std::string& text, double& value,
|
bool ParseDouble(const std::string& text, double& value,
|
||||||
const SourceLocation& location, const std::string& keyword) {
|
const SourceLocation& location, const std::string& keyword) {
|
||||||
if (text.empty()) {
|
if (text.empty()) {
|
||||||
@@ -311,16 +285,17 @@ class MappingContext {
|
|||||||
|
|
||||||
bool ContainsName(const std::vector<RawPart>& values,
|
bool ContainsName(const std::vector<RawPart>& values,
|
||||||
const std::string& name) const {
|
const std::string& name) const {
|
||||||
return std::any_of(
|
return std::any_of(values.begin(), values.end(),
|
||||||
values.begin(), values.end(),
|
[&name](const RawPart& value) {
|
||||||
[&name](const RawPart& value) { return EqualName(value.name, name); });
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ContainsName(const std::vector<RawInstance>& values,
|
bool ContainsName(const std::vector<RawInstance>& values,
|
||||||
const std::string& name) const {
|
const std::string& name) const {
|
||||||
return std::any_of(values.begin(), values.end(),
|
return std::any_of(values.begin(), values.end(),
|
||||||
[&name](const RawInstance& value) {
|
[&name](const RawInstance& value) {
|
||||||
return EqualName(value.name, name);
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,14 +303,14 @@ class MappingContext {
|
|||||||
const std::string& name) const {
|
const std::string& name) const {
|
||||||
return std::any_of(values.begin(), values.end(),
|
return std::any_of(values.begin(), values.end(),
|
||||||
[&name](const RawMaterial& value) {
|
[&name](const RawMaterial& value) {
|
||||||
return EqualName(value.name, name);
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ContainsSetName(const std::vector<RawSet>& sets,
|
bool ContainsSetName(const std::vector<RawSet>& sets,
|
||||||
const std::string& name) const {
|
const std::string& name) const {
|
||||||
const auto matches = [&name](const RawSet& set) {
|
const auto matches = [&name](const RawSet& set) {
|
||||||
return EqualName(set.name, name);
|
return AsciiCaseInsensitiveEquals(set.name, name);
|
||||||
};
|
};
|
||||||
return std::any_of(sets.begin(), sets.end(), matches);
|
return std::any_of(sets.begin(), sets.end(), matches);
|
||||||
}
|
}
|
||||||
@@ -344,7 +319,7 @@ class MappingContext {
|
|||||||
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
|
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
|
||||||
[node_set, &name](const RawAssemblySet& set) {
|
[node_set, &name](const RawAssemblySet& set) {
|
||||||
return set.is_node_set == node_set &&
|
return set.is_node_set == node_set &&
|
||||||
EqualName(set.name, name);
|
AsciiCaseInsensitiveEquals(set.name, name);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,15 +649,15 @@ class MappingContext {
|
|||||||
RawElement::Type element_type{};
|
RawElement::Type element_type{};
|
||||||
ElementFamily element_family{};
|
ElementFamily element_family{};
|
||||||
std::size_t expected_field_count = 0U;
|
std::size_t expected_field_count = 0U;
|
||||||
if (EqualName(*type, "B33")) {
|
if (AsciiCaseInsensitiveEquals(*type, "B33")) {
|
||||||
element_type = RawElement::Type::kB33;
|
element_type = RawElement::Type::kB33;
|
||||||
element_family = ElementFamily::kBeam;
|
element_family = ElementFamily::kBeam;
|
||||||
expected_field_count = 3U;
|
expected_field_count = 3U;
|
||||||
} else if (EqualName(*type, "S4")) {
|
} else if (AsciiCaseInsensitiveEquals(*type, "S4")) {
|
||||||
element_type = RawElement::Type::kS4;
|
element_type = RawElement::Type::kS4;
|
||||||
element_family = ElementFamily::kShell;
|
element_family = ElementFamily::kShell;
|
||||||
expected_field_count = 5U;
|
expected_field_count = 5U;
|
||||||
} else if (EqualName(*type, "S4R")) {
|
} else if (AsciiCaseInsensitiveEquals(*type, "S4R")) {
|
||||||
element_type = RawElement::Type::kS4r;
|
element_type = RawElement::Type::kS4r;
|
||||||
element_family = ElementFamily::kShell;
|
element_family = ElementFamily::kShell;
|
||||||
expected_field_count = 5U;
|
expected_field_count = 5U;
|
||||||
@@ -803,9 +778,7 @@ class MappingContext {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (block.data[0].fields.size() == 2U) {
|
if (block.data[0].fields.size() == 2U) {
|
||||||
std::int64_t ignored_integration_points = 0;
|
if (!ParsePositiveSourceLabel(block.data[0].fields[1]).HasValue()) {
|
||||||
if (!TryPositiveInteger(block.data[0].fields[1],
|
|
||||||
ignored_integration_points)) {
|
|
||||||
InputFailure("unsupported-shell-section-option", block.data[0].location,
|
InputFailure("unsupported-shell-section-option", block.data[0].location,
|
||||||
block.canonical_name, block.data[0].fields[1],
|
block.canonical_name, block.data[0].fields[1],
|
||||||
"The optional shell integration-point field must be a "
|
"The optional shell integration-point field must be a "
|
||||||
@@ -932,7 +905,7 @@ class MappingContext {
|
|||||||
if (element_set == nullptr || material == nullptr || section == nullptr) {
|
if (element_set == nullptr || material == nullptr || section == nullptr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!EqualName(*section, "GENERAL")) {
|
if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
|
||||||
InputFailure("unsupported-section-formulation", block.location,
|
InputFailure("unsupported-section-formulation", block.location,
|
||||||
block.canonical_name, *section,
|
block.canonical_name, *section,
|
||||||
"Only SECTION=GENERAL belongs to the approved subset.");
|
"Only SECTION=GENERAL belongs to the approved subset.");
|
||||||
@@ -940,7 +913,8 @@ class MappingContext {
|
|||||||
}
|
}
|
||||||
if (std::any_of(part.sections.begin(), part.sections.end(),
|
if (std::any_of(part.sections.begin(), part.sections.end(),
|
||||||
[element_set](const RawSection& existing) {
|
[element_set](const RawSection& existing) {
|
||||||
return EqualName(existing.element_set_name, *element_set);
|
return AsciiCaseInsensitiveEquals(
|
||||||
|
existing.element_set_name, *element_set);
|
||||||
})) {
|
})) {
|
||||||
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
||||||
*element_set,
|
*element_set,
|
||||||
@@ -1332,7 +1306,7 @@ class MappingContext {
|
|||||||
"NLGEOM requires NO in the approved subset.");
|
"NLGEOM requires NO in the approved subset.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!EqualName(*nlgeom->value, "NO")) {
|
if (!AsciiCaseInsensitiveEquals(*nlgeom->value, "NO")) {
|
||||||
if (model_element_family_ == ElementFamily::kShell) {
|
if (model_element_family_ == ElementFamily::kShell) {
|
||||||
InputFailure("unsupported-nonlinear-geometry", block.location,
|
InputFailure("unsupported-nonlinear-geometry", block.location,
|
||||||
block.canonical_name, *nlgeom->value,
|
block.canonical_name, *nlgeom->value,
|
||||||
@@ -1510,22 +1484,24 @@ class MappingContext {
|
|||||||
|
|
||||||
const RawPart* FindPart(const std::string& name) const {
|
const RawPart* FindPart(const std::string& name) const {
|
||||||
const auto found = std::find_if(
|
const auto found = std::find_if(
|
||||||
parts_.begin(), parts_.end(),
|
parts_.begin(), parts_.end(), [&name](const RawPart& part) {
|
||||||
[&name](const RawPart& part) { return EqualName(part.name, name); });
|
return AsciiCaseInsensitiveEquals(part.name, name);
|
||||||
|
});
|
||||||
return found == parts_.end() ? nullptr : &*found;
|
return found == parts_.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RawInstance* FindInstance(const std::string& name) const {
|
const RawInstance* FindInstance(const std::string& name) const {
|
||||||
const auto found = std::find_if(instances_.begin(), instances_.end(),
|
const auto found =
|
||||||
|
std::find_if(instances_.begin(), instances_.end(),
|
||||||
[&name](const RawInstance& instance) {
|
[&name](const RawInstance& instance) {
|
||||||
return EqualName(instance.name, name);
|
return AsciiCaseInsensitiveEquals(instance.name, name);
|
||||||
});
|
});
|
||||||
return found == instances_.end() ? nullptr : &*found;
|
return found == instances_.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const {
|
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const {
|
||||||
for (std::size_t index = 0U; index < materials_.size(); ++index) {
|
for (std::size_t index = 0U; index < materials_.size(); ++index) {
|
||||||
if (EqualName(materials_[index].name, name)) {
|
if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) {
|
||||||
return static_cast<EntityIndex>(index);
|
return static_cast<EntityIndex>(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1534,9 +1510,10 @@ class MappingContext {
|
|||||||
|
|
||||||
const RawSet* FindSet(const std::vector<RawSet>& sets,
|
const RawSet* FindSet(const std::vector<RawSet>& sets,
|
||||||
const std::string& name) const {
|
const std::string& name) const {
|
||||||
const auto found = std::find_if(
|
const auto found =
|
||||||
sets.begin(), sets.end(),
|
std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) {
|
||||||
[&name](const RawSet& set) { return EqualName(set.name, name); });
|
return AsciiCaseInsensitiveEquals(set.name, name);
|
||||||
|
});
|
||||||
return found == sets.end() ? nullptr : &*found;
|
return found == sets.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2000,7 +1977,8 @@ class MappingContext {
|
|||||||
const auto definition_instance = std::find_if(
|
const auto definition_instance = std::find_if(
|
||||||
definition_.instances.begin(), definition_.instances.end(),
|
definition_.instances.begin(), definition_.instances.end(),
|
||||||
[&raw_instance](const InstanceDefinition& instance) {
|
[&raw_instance](const InstanceDefinition& instance) {
|
||||||
return EqualName(instance.name, raw_instance->name);
|
return AsciiCaseInsensitiveEquals(instance.name,
|
||||||
|
raw_instance->name);
|
||||||
});
|
});
|
||||||
if (definition_instance == definition_.instances.end()) {
|
if (definition_instance == definition_.instances.end()) {
|
||||||
InputFailure("unresolved-reference", raw_set.location,
|
InputFailure("unresolved-reference", raw_set.location,
|
||||||
@@ -2010,10 +1988,10 @@ class MappingContext {
|
|||||||
}
|
}
|
||||||
if (raw_set.is_node_set) {
|
if (raw_set.is_node_set) {
|
||||||
definition_.node_sets.erase(
|
definition_.node_sets.erase(
|
||||||
std::remove_if(definition_.node_sets.begin(),
|
std::remove_if(
|
||||||
definition_.node_sets.end(),
|
definition_.node_sets.begin(), definition_.node_sets.end(),
|
||||||
[&raw_set](const NodeSet& set) {
|
[&raw_set](const NodeSet& set) {
|
||||||
return EqualName(set.name, raw_set.name);
|
return AsciiCaseInsensitiveEquals(set.name, raw_set.name);
|
||||||
}),
|
}),
|
||||||
definition_.node_sets.end());
|
definition_.node_sets.end());
|
||||||
NodeSet set{
|
NodeSet set{
|
||||||
@@ -2040,7 +2018,8 @@ class MappingContext {
|
|||||||
std::remove_if(definition_.element_sets.begin(),
|
std::remove_if(definition_.element_sets.begin(),
|
||||||
definition_.element_sets.end(),
|
definition_.element_sets.end(),
|
||||||
[&raw_set](const ElementSet& set) {
|
[&raw_set](const ElementSet& set) {
|
||||||
return EqualName(set.name, raw_set.name);
|
return AsciiCaseInsensitiveEquals(set.name,
|
||||||
|
raw_set.name);
|
||||||
}),
|
}),
|
||||||
definition_.element_sets.end());
|
definition_.element_sets.end());
|
||||||
ElementSet set{
|
ElementSet set{
|
||||||
@@ -2066,68 +2045,57 @@ class MappingContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
/// @brief Builds one stable node-target index from the completed candidate.
|
||||||
const std::string& target, const SourceLocation& location,
|
SourceTargetIndex BuildSourceTargetIndex() const {
|
||||||
const std::string& keyword) {
|
std::vector<SourceTargetIndexEntry> entries;
|
||||||
std::vector<const NodeSet*> matching_sets;
|
std::size_t declaration_order = 0U;
|
||||||
|
for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) {
|
||||||
|
const auto& source_id = definition_.nodes[node].source_id;
|
||||||
|
entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "",
|
||||||
|
source_id, static_cast<EntityIndex>(node),
|
||||||
|
declaration_order++});
|
||||||
|
}
|
||||||
for (const auto& set : definition_.node_sets) {
|
for (const auto& set : definition_.node_sets) {
|
||||||
if (EqualName(set.name, target)) {
|
for (const EntityIndex node : set.node_indices) {
|
||||||
matching_sets.push_back(&set);
|
const auto& source_id = definition_.nodes[node].source_id;
|
||||||
|
entries.push_back({SourceEntityKind::kNode,
|
||||||
|
set.instance_name.value_or(source_id.instance_name),
|
||||||
|
set.name, source_id, node, declaration_order++});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return SourceTargetIndex{std::move(entries)};
|
||||||
|
}
|
||||||
|
|
||||||
std::vector<EntityIndex> matching_nodes;
|
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
||||||
std::int64_t label = 0;
|
const SourceTargetResolver& resolver, const std::string& target,
|
||||||
if (TryPositiveInteger(target, label)) {
|
const SourceLocation& location, const std::string& keyword) {
|
||||||
for (std::size_t index = 0U; index < definition_.nodes.size(); ++index) {
|
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
|
||||||
if (definition_.nodes[index].source_id.source_label == label) {
|
if (!resolved.HasValue()) {
|
||||||
matching_nodes.push_back(static_cast<EntityIndex>(index));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A token is resolved only after both approved interpretations have
|
|
||||||
// been considered; declaration order never gives a node set priority
|
|
||||||
// over an equally valid direct source-label target.
|
|
||||||
if (matching_sets.size() > 1U) {
|
|
||||||
InputFailure(
|
InputFailure(
|
||||||
"unresolved-reference", location, keyword, target,
|
"unresolved-reference", location, keyword, target,
|
||||||
"The node-set target is ambiguous across identity instances.");
|
"The boundary or load target must resolve unambiguously to one "
|
||||||
|
"node or one node set.");
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
if (matching_nodes.size() > 1U) {
|
std::vector<EntityIndex> indices;
|
||||||
InputFailure("unresolved-reference", location, keyword, target,
|
indices.reserve(resolved.Value().size());
|
||||||
"The direct node-label target is ambiguous across identity "
|
for (const auto& entry : resolved.Value()) {
|
||||||
"instances.");
|
indices.push_back(entry.entity_index);
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
if (matching_sets.size() == 1U && matching_nodes.size() == 1U) {
|
return indices;
|
||||||
InputFailure("unresolved-reference", location, keyword, target,
|
|
||||||
"The target is ambiguous between a node-set name and a "
|
|
||||||
"direct node label.");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
if (matching_sets.size() == 1U) {
|
|
||||||
return matching_sets.front()->node_indices;
|
|
||||||
}
|
|
||||||
if (matching_nodes.size() == 1U) {
|
|
||||||
return matching_nodes;
|
|
||||||
}
|
|
||||||
InputFailure(
|
|
||||||
"unresolved-reference", location, keyword, target,
|
|
||||||
"The boundary or load target must resolve to one node or node set.");
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void FinalizeStep() {
|
void FinalizeStep() {
|
||||||
std::vector<BoundaryCondition> boundaries = model_boundaries_;
|
std::vector<BoundaryCondition> boundaries = model_boundaries_;
|
||||||
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
|
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
|
||||||
step_.boundaries.end());
|
step_.boundaries.end());
|
||||||
|
const SourceTargetIndex target_index = BuildSourceTargetIndex();
|
||||||
|
const SourceTargetResolver target_resolver{target_index};
|
||||||
|
|
||||||
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
|
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
|
||||||
for (const auto& boundary : boundaries) {
|
for (const auto& boundary : boundaries) {
|
||||||
auto target =
|
auto target = ResolveNodeTarget(target_resolver, boundary.target,
|
||||||
ResolveNodeTarget(boundary.target, boundary.location, "BOUNDARY");
|
boundary.location, "BOUNDARY");
|
||||||
if (!target) {
|
if (!target) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -2148,7 +2116,8 @@ class MappingContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const auto& load : step_.loads) {
|
for (const auto& load : step_.loads) {
|
||||||
if (!ResolveNodeTarget(load.target, load.location, "CLOAD")) {
|
if (!ResolveNodeTarget(target_resolver, load.target, load.location,
|
||||||
|
"CLOAD")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/ascii.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Match {
|
||||||
|
const SourceTargetIndexEntry* entry;
|
||||||
|
bool named_target;
|
||||||
|
std::size_t input_order;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool InstanceMatches(const std::string& query_instance,
|
||||||
|
const std::string& entry_instance) {
|
||||||
|
return query_instance.empty() ||
|
||||||
|
AsciiCaseInsensitiveEquals(query_instance, entry_instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LooksLikeInteger(const std::string& text) {
|
||||||
|
if (text.empty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::size_t offset = 0U;
|
||||||
|
if (text.front() == '+' || text.front() == '-') {
|
||||||
|
offset = 1U;
|
||||||
|
}
|
||||||
|
if (offset == text.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return std::all_of(
|
||||||
|
text.begin() + static_cast<std::ptrdiff_t>(offset), text.end(),
|
||||||
|
[](const char value) { return value >= '0' && value <= '9'; });
|
||||||
|
}
|
||||||
|
|
||||||
|
Diagnostic TargetDiagnostic(const std::string& code,
|
||||||
|
const std::string& identity,
|
||||||
|
const std::string& message) {
|
||||||
|
return {Severity::kError, code, {{}, 0U}, "SOURCE_TARGET", identity, message};
|
||||||
|
}
|
||||||
|
|
||||||
|
Result<std::vector<ResolvedSourceTarget>> TargetFailure(
|
||||||
|
const std::string& code, const std::string& identity,
|
||||||
|
const std::string& message) {
|
||||||
|
return Result<std::vector<ResolvedSourceTarget>>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kInput, {TargetDiagnostic(code, identity, message)}));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SpansInstances(const std::vector<Match>& matches, const bool named) {
|
||||||
|
const SourceTargetIndexEntry* first = nullptr;
|
||||||
|
for (const auto& match : matches) {
|
||||||
|
if (match.named_target != named) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (first == nullptr) {
|
||||||
|
first = match.entry;
|
||||||
|
} else if (!AsciiCaseInsensitiveEquals(first->instance_name,
|
||||||
|
match.entry->instance_name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
SourceTargetIndex::SourceTargetIndex(
|
||||||
|
std::vector<SourceTargetIndexEntry> entries)
|
||||||
|
: entries_{std::move(entries)} {}
|
||||||
|
|
||||||
|
SourceTargetIndex SourceTargetIndex::FromDomain(const Domain& domain) {
|
||||||
|
std::vector<SourceTargetIndexEntry> entries;
|
||||||
|
std::size_t declaration_order = 0U;
|
||||||
|
for (std::size_t index = 0U; index < domain.Nodes().size(); ++index) {
|
||||||
|
const auto& node = domain.Nodes()[index];
|
||||||
|
entries.push_back({SourceEntityKind::kNode, node.source_id.instance_name,
|
||||||
|
"", node.source_id, static_cast<EntityIndex>(index),
|
||||||
|
declaration_order++});
|
||||||
|
}
|
||||||
|
for (const auto& set : domain.NodeSets()) {
|
||||||
|
for (const EntityIndex node_index : set.node_indices) {
|
||||||
|
SourceEntityId source_id{set.instance_name.value_or(""), 0, ""};
|
||||||
|
if (node_index < domain.Nodes().size()) {
|
||||||
|
source_id = domain.Nodes()[node_index].source_id;
|
||||||
|
}
|
||||||
|
entries.push_back({SourceEntityKind::kNode,
|
||||||
|
set.instance_name.value_or(source_id.instance_name),
|
||||||
|
set.name, std::move(source_id), node_index,
|
||||||
|
declaration_order++});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (std::size_t index = 0U; index < domain.Elements().size(); ++index) {
|
||||||
|
const auto& element = domain.Elements()[index];
|
||||||
|
entries.push_back({SourceEntityKind::kElement,
|
||||||
|
element.source_id.instance_name, "", element.source_id,
|
||||||
|
static_cast<EntityIndex>(index), declaration_order++});
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0U; index < domain.ShellElements().size(); ++index) {
|
||||||
|
const auto& element = domain.ShellElements()[index];
|
||||||
|
entries.push_back({SourceEntityKind::kElement,
|
||||||
|
element.source_id.instance_name, "", element.source_id,
|
||||||
|
static_cast<EntityIndex>(index), declaration_order++});
|
||||||
|
}
|
||||||
|
for (const auto& set : domain.ElementSets()) {
|
||||||
|
for (const EntityIndex element_index : set.element_indices) {
|
||||||
|
SourceEntityId source_id{set.instance_name.value_or(""), 0, ""};
|
||||||
|
if (domain.ShellElements().empty() &&
|
||||||
|
element_index < domain.Elements().size()) {
|
||||||
|
source_id = domain.Elements()[element_index].source_id;
|
||||||
|
} else if (domain.Elements().empty() &&
|
||||||
|
element_index < domain.ShellElements().size()) {
|
||||||
|
source_id = domain.ShellElements()[element_index].source_id;
|
||||||
|
}
|
||||||
|
entries.push_back({SourceEntityKind::kElement,
|
||||||
|
set.instance_name.value_or(source_id.instance_name),
|
||||||
|
set.name, std::move(source_id), element_index,
|
||||||
|
declaration_order++});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SourceTargetIndex{std::move(entries)};
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<SourceTargetIndexEntry>& SourceTargetIndex::Entries()
|
||||||
|
const noexcept {
|
||||||
|
return entries_;
|
||||||
|
}
|
||||||
|
|
||||||
|
SourceTargetResolver::SourceTargetResolver(
|
||||||
|
const SourceTargetIndex& index) noexcept
|
||||||
|
: index_{&index} {}
|
||||||
|
|
||||||
|
Result<std::vector<ResolvedSourceTarget>> SourceTargetResolver::Resolve(
|
||||||
|
const SourceTargetQuery& query) const {
|
||||||
|
const auto parsed_label =
|
||||||
|
ParsePositiveSourceLabel(query.target_name_or_label);
|
||||||
|
std::vector<Match> matches;
|
||||||
|
for (std::size_t input_order = 0U; input_order < index_->Entries().size();
|
||||||
|
++input_order) {
|
||||||
|
const auto& entry = index_->Entries()[input_order];
|
||||||
|
if (entry.entity_kind != query.entity_kind ||
|
||||||
|
!InstanceMatches(query.instance_name, entry.instance_name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const bool named = !entry.target_name.empty() &&
|
||||||
|
AsciiCaseInsensitiveEquals(entry.target_name,
|
||||||
|
query.target_name_or_label);
|
||||||
|
const bool direct = entry.target_name.empty() && parsed_label.HasValue() &&
|
||||||
|
entry.source_id.source_label == parsed_label.Value();
|
||||||
|
if (named || direct) {
|
||||||
|
matches.push_back({&entry, named, input_order});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.empty()) {
|
||||||
|
if (!parsed_label.HasValue() &&
|
||||||
|
LooksLikeInteger(query.target_name_or_label)) {
|
||||||
|
return TargetFailure(
|
||||||
|
"invalid-source-target", query.target_name_or_label,
|
||||||
|
"A direct source target must be a positive base-10 label.");
|
||||||
|
}
|
||||||
|
return TargetFailure(
|
||||||
|
"unresolved-source-target", query.target_name_or_label,
|
||||||
|
"The source target does not resolve in its namespace.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool has_named =
|
||||||
|
std::any_of(matches.begin(), matches.end(),
|
||||||
|
[](const Match& match) { return match.named_target; });
|
||||||
|
const bool has_direct =
|
||||||
|
std::any_of(matches.begin(), matches.end(),
|
||||||
|
[](const Match& match) { return !match.named_target; });
|
||||||
|
if ((has_named && has_direct) || SpansInstances(matches, true) ||
|
||||||
|
SpansInstances(matches, false)) {
|
||||||
|
return TargetFailure(
|
||||||
|
"ambiguous-source-target", query.target_name_or_label,
|
||||||
|
"The source target has more than one valid identity interpretation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::stable_sort(
|
||||||
|
matches.begin(), matches.end(), [](const Match& lhs, const Match& rhs) {
|
||||||
|
if (lhs.entry->declaration_order != rhs.entry->declaration_order) {
|
||||||
|
return lhs.entry->declaration_order < rhs.entry->declaration_order;
|
||||||
|
}
|
||||||
|
return lhs.input_order < rhs.input_order;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::vector<Diagnostic> duplicate_diagnostics;
|
||||||
|
std::vector<EntityIndex> seen_indices;
|
||||||
|
for (const auto& match : matches) {
|
||||||
|
const auto duplicate = std::find(seen_indices.begin(), seen_indices.end(),
|
||||||
|
match.entry->entity_index);
|
||||||
|
if (duplicate != seen_indices.end()) {
|
||||||
|
duplicate_diagnostics.push_back(TargetDiagnostic(
|
||||||
|
"duplicate-source-target", query.target_name_or_label,
|
||||||
|
"Duplicate resolved source target " +
|
||||||
|
match.entry->source_id.source_label_text + "."));
|
||||||
|
} else {
|
||||||
|
seen_indices.push_back(match.entry->entity_index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!duplicate_diagnostics.empty()) {
|
||||||
|
return Result<std::vector<ResolvedSourceTarget>>::Failure(Status::Failure(
|
||||||
|
FailureCategory::kInput, std::move(duplicate_diagnostics)));
|
||||||
|
}
|
||||||
|
if (!has_named && seen_indices.size() > 1U) {
|
||||||
|
return TargetFailure(
|
||||||
|
"ambiguous-source-target", query.target_name_or_label,
|
||||||
|
"The direct source label resolves to multiple semantic entities.");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ResolvedSourceTarget> resolved;
|
||||||
|
resolved.reserve(matches.size());
|
||||||
|
for (const auto& match : matches) {
|
||||||
|
resolved.push_back({match.entry->source_id, match.entry->entity_index});
|
||||||
|
}
|
||||||
|
return Result<std::vector<ResolvedSourceTarget>>::Success(
|
||||||
|
std::move(resolved));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -2,21 +2,19 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <charconv>
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <cstdint>
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <system_error>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "fesa/elements/euler_beam_3d.h"
|
#include "fesa/elements/euler_beam_3d.h"
|
||||||
#include "fesa/elements/mitc4_shell.h"
|
#include "fesa/elements/mitc4_shell.h"
|
||||||
#include "fesa/math/vector3.h"
|
#include "fesa/math/vector3.h"
|
||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
@@ -324,70 +322,29 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
|
|||||||
return Status::Ok();
|
return Status::Ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
char AsciiLower(const char value) {
|
Result<std::vector<EntityIndex>> ResolveLoadTarget(
|
||||||
if (value >= 'A' && value <= 'Z') {
|
const SourceTargetResolver& resolver, const Domain& domain,
|
||||||
return static_cast<char>(value + ('a' - 'A'));
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool EqualName(const std::string& left, const std::string& right) {
|
|
||||||
return left.size() == right.size() &&
|
|
||||||
std::equal(left.begin(), left.end(), right.begin(),
|
|
||||||
[](const char left_value, const char right_value) {
|
|
||||||
return AsciiLower(left_value) == AsciiLower(right_value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
bool TryPositiveInteger(const std::string& text, std::int64_t& value) {
|
|
||||||
const char* const first = text.data();
|
|
||||||
const char* const last = first + text.size();
|
|
||||||
const auto parsed = std::from_chars(first, last, value);
|
|
||||||
return parsed.ec == std::errc{} && parsed.ptr == last && value > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result<std::vector<EntityIndex>> ResolveLoadTarget(const Domain& domain,
|
|
||||||
const NodalLoad& load) {
|
const NodalLoad& load) {
|
||||||
std::vector<const NodeSet*> sets;
|
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
|
||||||
for (const auto& set : domain.NodeSets()) {
|
if (!resolved.HasValue()) {
|
||||||
if (EqualName(set.name, load.target)) {
|
|
||||||
sets.push_back(&set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::vector<EntityIndex> nodes;
|
|
||||||
std::int64_t source_label = 0;
|
|
||||||
if (TryPositiveInteger(load.target, source_label)) {
|
|
||||||
for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) {
|
|
||||||
if (domain.Nodes()[node].source_id.source_label == source_label) {
|
|
||||||
nodes.push_back(static_cast<EntityIndex>(node));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (sets.size() > 1U || nodes.size() > 1U ||
|
|
||||||
(!sets.empty() && !nodes.empty())) {
|
|
||||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
||||||
"invalid-node-station-entity", load.location, load.target,
|
"invalid-node-station-entity", load.location, load.target,
|
||||||
"A station-eligibility load target must resolve unambiguously.");
|
"A station-eligibility load target must resolve unambiguously.");
|
||||||
}
|
}
|
||||||
if (!sets.empty()) {
|
std::vector<EntityIndex> nodes;
|
||||||
|
nodes.reserve(resolved.Value().size());
|
||||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||||
for (const EntityIndex node : sets.front()->node_indices) {
|
for (const auto& target : resolved.Value()) {
|
||||||
|
const EntityIndex node = target.entity_index;
|
||||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
||||||
"invalid-node-station-entity", load.location, load.target,
|
"invalid-node-station-entity", load.location, load.target,
|
||||||
"A station-eligibility node set must contain unique valid nodes.");
|
"A station-eligibility node set must contain unique valid nodes.");
|
||||||
}
|
}
|
||||||
seen[node] = 1U;
|
seen[node] = 1U;
|
||||||
|
nodes.push_back(node);
|
||||||
}
|
}
|
||||||
return Result<std::vector<EntityIndex>>::Success(
|
|
||||||
sets.front()->node_indices);
|
|
||||||
}
|
|
||||||
if (!nodes.empty()) {
|
|
||||||
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
|
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
|
||||||
}
|
|
||||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
|
||||||
"invalid-node-station-entity", load.location, load.target,
|
|
||||||
"A station-eligibility load target must resolve to a node or node set.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale,
|
bool AccumulateVectorAndScale(std::array<double, 3>& total, double& scale,
|
||||||
@@ -879,6 +836,8 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
|
|||||||
"invalid-node-station-entity", model.Step().location, model.Step().name,
|
"invalid-node-station-entity", model.Step().location, model.Step().name,
|
||||||
"The active load view must preserve every sole-step load.");
|
"The active load view must preserve every sole-step load.");
|
||||||
}
|
}
|
||||||
|
const SourceTargetIndex target_index = SourceTargetIndex::FromDomain(domain);
|
||||||
|
const SourceTargetResolver target_resolver{target_index};
|
||||||
for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) {
|
for (std::size_t order = 0U; order < model.ActiveLoads().size(); ++order) {
|
||||||
const EntityIndex load_index = model.ActiveLoads()[order];
|
const EntityIndex load_index = model.ActiveLoads()[order];
|
||||||
if (load_index != order || load_index >= model.Step().loads.size()) {
|
if (load_index != order || load_index >= model.Step().loads.size()) {
|
||||||
@@ -893,7 +852,7 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
|
|||||||
"nonfinite-node-station-value", load.location, load.target,
|
"nonfinite-node-station-value", load.location, load.target,
|
||||||
"Station eligibility requires finite concentrated loads.");
|
"Station eligibility requires finite concentrated loads.");
|
||||||
}
|
}
|
||||||
auto targets = ResolveLoadTarget(domain, load);
|
auto targets = ResolveLoadTarget(target_resolver, domain, load);
|
||||||
if (!targets.HasValue()) {
|
if (!targets.HasValue()) {
|
||||||
return Result<std::vector<NodeStationResultRow>>::Failure(
|
return Result<std::vector<NodeStationResultRow>>::Failure(
|
||||||
targets.GetStatus());
|
targets.GetStatus());
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ add_executable(
|
|||||||
unit/assembly/load_assembler_test.cpp
|
unit/assembly/load_assembler_test.cpp
|
||||||
unit/assembly/sparse_assembler_test.cpp
|
unit/assembly/sparse_assembler_test.cpp
|
||||||
unit/constraints/essential_constraints_test.cpp
|
unit/constraints/essential_constraints_test.cpp
|
||||||
|
unit/core/ascii_test.cpp
|
||||||
unit/core/diagnostic_test.cpp
|
unit/core/diagnostic_test.cpp
|
||||||
unit/core/source_identity_test.cpp
|
unit/core/source_identity_test.cpp
|
||||||
unit/core/status_test.cpp
|
unit/core/status_test.cpp
|
||||||
@@ -27,6 +28,7 @@ add_executable(
|
|||||||
unit/model/domain_test.cpp
|
unit/model/domain_test.cpp
|
||||||
unit/model/model_types_test.cpp
|
unit/model/model_types_test.cpp
|
||||||
unit/model/shell_geometry_test.cpp
|
unit/model/shell_geometry_test.cpp
|
||||||
|
unit/model/source_target_resolver_test.cpp
|
||||||
unit/results/result_records_test.cpp
|
unit/results/result_records_test.cpp
|
||||||
unit/results/result_recovery_test.cpp
|
unit/results/result_recovery_test.cpp
|
||||||
unit/results/results_writer_test.cpp
|
unit/results/results_writer_test.cpp
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
#include "fesa/core/ascii.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
TEST(Ascii, LowercasesOnlyAsciiUppercaseBytes) {
|
||||||
|
EXPECT_EQ(fesa::AsciiLower('A'), 'a');
|
||||||
|
EXPECT_EQ(fesa::AsciiLower('Z'), 'z');
|
||||||
|
EXPECT_EQ(fesa::AsciiLower('a'), 'a');
|
||||||
|
EXPECT_EQ(fesa::AsciiLower('0'), '0');
|
||||||
|
|
||||||
|
const char non_ascii = static_cast<char>(0xc0U);
|
||||||
|
EXPECT_EQ(fesa::AsciiLower(non_ascii), non_ascii);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Ascii, ComparesNamesWithoutLocaleOrUnicodeFolding) {
|
||||||
|
EXPECT_TRUE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "bEaM-1"));
|
||||||
|
EXPECT_FALSE(fesa::AsciiCaseInsensitiveEquals("Beam-1", "Beam-10"));
|
||||||
|
|
||||||
|
const std::string uppercase_byte{static_cast<char>(0xc0U)};
|
||||||
|
const std::string lowercase_byte{static_cast<char>(0xe0U)};
|
||||||
|
EXPECT_FALSE(
|
||||||
|
fesa::AsciiCaseInsensitiveEquals(uppercase_byte, lowercase_byte));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Ascii, ParsesOnlyCompletePositiveBaseTenSourceLabels) {
|
||||||
|
auto one = fesa::ParsePositiveSourceLabel("1");
|
||||||
|
auto leading_zeroes = fesa::ParsePositiveSourceLabel("00042");
|
||||||
|
auto maximum = fesa::ParsePositiveSourceLabel(
|
||||||
|
std::to_string((std::numeric_limits<std::int64_t>::max)()));
|
||||||
|
|
||||||
|
ASSERT_TRUE(one.HasValue());
|
||||||
|
EXPECT_EQ(one.Value(), 1);
|
||||||
|
ASSERT_TRUE(leading_zeroes.HasValue());
|
||||||
|
EXPECT_EQ(leading_zeroes.Value(), 42);
|
||||||
|
ASSERT_TRUE(maximum.HasValue());
|
||||||
|
EXPECT_EQ(maximum.Value(), (std::numeric_limits<std::int64_t>::max)());
|
||||||
|
|
||||||
|
const std::string overflow =
|
||||||
|
std::to_string((std::numeric_limits<std::int64_t>::max)()) + "0";
|
||||||
|
const std::vector<std::string> invalid_values{"", "0", "-1", "+", "+1",
|
||||||
|
" 1", "1 ", "1x", overflow};
|
||||||
|
for (const std::string& invalid : invalid_values) {
|
||||||
|
SCOPED_TRACE(invalid);
|
||||||
|
auto result = fesa::ParsePositiveSourceLabel(invalid);
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
|
||||||
|
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U);
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"invalid-source-label");
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].entity_identity, invalid);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
#include "fesa/model/source_target_resolver.h"
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::SourceTargetIndexEntry Entry(const fesa::SourceEntityKind entity_kind,
|
||||||
|
std::string instance_name,
|
||||||
|
std::string target_name,
|
||||||
|
const std::int64_t source_label,
|
||||||
|
std::string source_label_text,
|
||||||
|
const fesa::EntityIndex entity_index,
|
||||||
|
const std::size_t declaration_order) {
|
||||||
|
fesa::SourceEntityId source_id{instance_name, source_label,
|
||||||
|
std::move(source_label_text)};
|
||||||
|
return {entity_kind,
|
||||||
|
std::move(instance_name),
|
||||||
|
std::move(target_name),
|
||||||
|
std::move(source_id),
|
||||||
|
entity_index,
|
||||||
|
declaration_order};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<fesa::EntityIndex> Indices(
|
||||||
|
const std::vector<fesa::ResolvedSourceTarget>& targets) {
|
||||||
|
std::vector<fesa::EntityIndex> indices;
|
||||||
|
indices.reserve(targets.size());
|
||||||
|
for (const auto& target : targets) {
|
||||||
|
indices.push_back(target.entity_index);
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExpectFailure(const fesa::SourceTargetIndex& index,
|
||||||
|
fesa::SourceTargetQuery query, const std::string& code) {
|
||||||
|
const auto result = fesa::SourceTargetResolver{index}.Resolve(query);
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
|
||||||
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code, code);
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].entity_identity,
|
||||||
|
query.target_name_or_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST(SourceTargetResolver,
|
||||||
|
SeparatesNamespacesAndInstancesAndExpandsInDeclarationOrder) {
|
||||||
|
using fesa::SourceEntityKind;
|
||||||
|
fesa::SourceTargetIndex index{{
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Ends", 20, "0020", 1U, 30U),
|
||||||
|
Entry(SourceEntityKind::kElement, "Beam-1", "Ends", 700, "0700", 4U, 10U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Ends", 10, "0010", 0U, 20U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-2", "Ends", 10, "0010", 2U, 40U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "", 10, "0010", 0U, 0U),
|
||||||
|
}};
|
||||||
|
const fesa::SourceTargetResolver resolver{index};
|
||||||
|
|
||||||
|
auto nodes = resolver.Resolve({SourceEntityKind::kNode, "bEaM-1", "eNdS"});
|
||||||
|
ASSERT_TRUE(nodes.HasValue());
|
||||||
|
EXPECT_EQ(Indices(nodes.Value()), (std::vector<fesa::EntityIndex>{0U, 1U}));
|
||||||
|
EXPECT_EQ(nodes.Value()[0U].source_id.instance_name, "Beam-1");
|
||||||
|
EXPECT_EQ(nodes.Value()[0U].source_id.source_label_text, "0010");
|
||||||
|
|
||||||
|
auto elements =
|
||||||
|
resolver.Resolve({SourceEntityKind::kElement, "BEAM-1", "ends"});
|
||||||
|
ASSERT_TRUE(elements.HasValue());
|
||||||
|
EXPECT_EQ(Indices(elements.Value()), (std::vector<fesa::EntityIndex>{4U}));
|
||||||
|
|
||||||
|
auto second_instance =
|
||||||
|
resolver.Resolve({SourceEntityKind::kNode, "beam-2", "ENDS"});
|
||||||
|
ASSERT_TRUE(second_instance.HasValue());
|
||||||
|
EXPECT_EQ(Indices(second_instance.Value()),
|
||||||
|
(std::vector<fesa::EntityIndex>{2U}));
|
||||||
|
|
||||||
|
auto direct = resolver.Resolve({SourceEntityKind::kNode, "Beam-1", "10"});
|
||||||
|
ASSERT_TRUE(direct.HasValue());
|
||||||
|
EXPECT_EQ(Indices(direct.Value()), (std::vector<fesa::EntityIndex>{0U}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SourceTargetResolver, RejectsMissingAmbiguousAndNonpositiveTargets) {
|
||||||
|
using fesa::SourceEntityKind;
|
||||||
|
const fesa::SourceTargetIndex index{{
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "", 10, "10", 0U, 0U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-2", "", 10, "10", 1U, 1U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "10", 20, "20", 2U, 2U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Shared", 10, "10", 0U, 3U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-2", "Shared", 10, "10", 1U, 4U),
|
||||||
|
}};
|
||||||
|
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "", "Missing"},
|
||||||
|
"unresolved-source-target");
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "", "0"},
|
||||||
|
"invalid-source-target");
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "", "-7"},
|
||||||
|
"invalid-source-target");
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "", "10"},
|
||||||
|
"ambiguous-source-target");
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "", "Shared"},
|
||||||
|
"ambiguous-source-target");
|
||||||
|
ExpectFailure(index, {SourceEntityKind::kNode, "Beam-1", "10"},
|
||||||
|
"ambiguous-source-target");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(SourceTargetResolver,
|
||||||
|
ReportsDuplicateMembershipsInDeterministicDeclarationOrder) {
|
||||||
|
using fesa::SourceEntityKind;
|
||||||
|
const fesa::SourceTargetIndex index{{
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 10, "10", 0U, 1U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 10, "10", 0U, 9U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 20, "20", 1U, 2U),
|
||||||
|
Entry(SourceEntityKind::kNode, "Beam-1", "Duplicate", 20, "20", 1U, 7U),
|
||||||
|
}};
|
||||||
|
|
||||||
|
const auto result = fesa::SourceTargetResolver{index}.Resolve(
|
||||||
|
{SourceEntityKind::kNode, "Beam-1", "duplicate"});
|
||||||
|
|
||||||
|
ASSERT_FALSE(result.HasValue());
|
||||||
|
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
|
||||||
|
ASSERT_EQ(result.GetStatus().Diagnostics().size(), 2U);
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].code,
|
||||||
|
"duplicate-source-target");
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[0U].message,
|
||||||
|
"Duplicate resolved source target 20.");
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[1U].code,
|
||||||
|
"duplicate-source-target");
|
||||||
|
EXPECT_EQ(result.GetStatus().Diagnostics()[1U].message,
|
||||||
|
"Duplicate resolved source target 10.");
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user