feat(cpp-object-oriented-modular-refactoring): step 11 - source-target-resolver
This commit is contained in:
@@ -10,6 +10,7 @@ add_library(
|
||||
assembly/sparse_assembler.cpp
|
||||
build_info.cpp
|
||||
constraints/essential_constraints.cpp
|
||||
core/ascii.cpp
|
||||
core/diagnostic.cpp
|
||||
core/status.cpp
|
||||
elements/euler_beam_3d.cpp
|
||||
@@ -24,6 +25,7 @@ add_library(
|
||||
math/vector.cpp
|
||||
model/domain.cpp
|
||||
model/shell_geometry.cpp
|
||||
model/source_target_resolver.cpp
|
||||
results/result_recovery.cpp
|
||||
solvers/linear/mkl_pardiso_solver.cpp
|
||||
)
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
#include "fesa/assembly/load_assembler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/constraints/essential_constraints.h"
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
@@ -27,28 +25,6 @@ Status LoadFailure(const std::string& code, const SourceLocation& location,
|
||||
{{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.
|
||||
bool IsStrictlyIncreasing(const std::vector<std::size_t>& values) {
|
||||
return std::adjacent_find(
|
||||
@@ -122,52 +98,31 @@ Status ValidateDofOrder(const DofManager& dofs,
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
Result<std::vector<EntityIndex>> ResolveTarget(const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
std::vector<const NodeSet*> matching_sets;
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
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())) {
|
||||
Result<std::vector<EntityIndex>> ResolveTarget(
|
||||
const SourceTargetResolver& resolver, const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
|
||||
if (!resolved.HasValue()) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(
|
||||
LoadFailure("invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The load target must resolve unambiguously to one node or "
|
||||
"one expanded node set."));
|
||||
}
|
||||
if (!matching_sets.empty()) {
|
||||
const auto& nodes = matching_sets.front()->node_indices;
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const EntityIndex node : nodes) {
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The expanded node set must contain unique in-range stable node "
|
||||
"identities."));
|
||||
}
|
||||
seen[node] = 1U;
|
||||
std::vector<EntityIndex> nodes;
|
||||
nodes.reserve(resolved.Value().size());
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const auto& target : resolved.Value()) {
|
||||
const EntityIndex node = target.entity_index;
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return Result<std::vector<EntityIndex>>::Failure(LoadFailure(
|
||||
"invalid-load-target", load.location, "CLOAD", load.target,
|
||||
"The expanded node set must contain unique in-range stable node "
|
||||
"identities."));
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Success(nodes);
|
||||
seen[node] = 1U;
|
||||
nodes.push_back(node);
|
||||
}
|
||||
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."));
|
||||
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
|
||||
}
|
||||
|
||||
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,
|
||||
"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};
|
||||
// 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."));
|
||||
}
|
||||
|
||||
auto target = ResolveTarget(domain, load);
|
||||
auto target = ResolveTarget(target_resolver, domain, load);
|
||||
if (!target.HasValue()) {
|
||||
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 <algorithm>
|
||||
#include <charconv>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
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(
|
||||
const Domain& domain, const BoundaryCondition& boundary) {
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
if (EqualName(set.name, boundary.target)) {
|
||||
return set.node_indices;
|
||||
}
|
||||
const SourceTargetResolver& resolver, const BoundaryCondition& boundary) {
|
||||
auto resolved =
|
||||
resolver.Resolve({SourceEntityKind::kNode, "", boundary.target});
|
||||
if (!resolved.HasValue()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
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)};
|
||||
}
|
||||
}
|
||||
std::vector<EntityIndex> indices;
|
||||
indices.reserve(resolved.Value().size());
|
||||
for (const auto& target : resolved.Value()) {
|
||||
indices.push_back(target.entity_index);
|
||||
}
|
||||
return {};
|
||||
return indices;
|
||||
}
|
||||
|
||||
template <std::size_t scatter_size>
|
||||
@@ -94,12 +67,14 @@ SparsePattern BuildSparsePattern(
|
||||
|
||||
Result<DofManager> DofManager::Create(const AnalysisModel& model) {
|
||||
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;
|
||||
|
||||
std::vector<std::optional<double>> prescribed_by_full_dof(full_count);
|
||||
for (const EntityIndex boundary_index : model.ActiveBoundaryConditions()) {
|
||||
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 (int component = boundary.first_dof; component <= boundary.last_dof;
|
||||
++component) {
|
||||
|
||||
@@ -14,26 +14,14 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/ascii.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "fesa/model/shell_geometry.h"
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
|
||||
namespace fesa {
|
||||
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) {
|
||||
while (!fields.empty() && fields.back().empty()) {
|
||||
fields.pop_back();
|
||||
@@ -254,20 +242,6 @@ class MappingContext {
|
||||
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,
|
||||
const SourceLocation& location, const std::string& keyword) {
|
||||
if (text.empty()) {
|
||||
@@ -311,16 +285,17 @@ class MappingContext {
|
||||
|
||||
bool ContainsName(const std::vector<RawPart>& values,
|
||||
const std::string& name) const {
|
||||
return std::any_of(
|
||||
values.begin(), values.end(),
|
||||
[&name](const RawPart& value) { return EqualName(value.name, name); });
|
||||
return std::any_of(values.begin(), values.end(),
|
||||
[&name](const RawPart& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, name);
|
||||
});
|
||||
}
|
||||
|
||||
bool ContainsName(const std::vector<RawInstance>& values,
|
||||
const std::string& name) const {
|
||||
return std::any_of(values.begin(), values.end(),
|
||||
[&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 {
|
||||
return std::any_of(values.begin(), values.end(),
|
||||
[&name](const RawMaterial& value) {
|
||||
return EqualName(value.name, name);
|
||||
return AsciiCaseInsensitiveEquals(value.name, name);
|
||||
});
|
||||
}
|
||||
|
||||
bool ContainsSetName(const std::vector<RawSet>& sets,
|
||||
const std::string& name) const {
|
||||
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);
|
||||
}
|
||||
@@ -344,7 +319,7 @@ class MappingContext {
|
||||
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
|
||||
[node_set, &name](const RawAssemblySet& 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{};
|
||||
ElementFamily element_family{};
|
||||
std::size_t expected_field_count = 0U;
|
||||
if (EqualName(*type, "B33")) {
|
||||
if (AsciiCaseInsensitiveEquals(*type, "B33")) {
|
||||
element_type = RawElement::Type::kB33;
|
||||
element_family = ElementFamily::kBeam;
|
||||
expected_field_count = 3U;
|
||||
} else if (EqualName(*type, "S4")) {
|
||||
} else if (AsciiCaseInsensitiveEquals(*type, "S4")) {
|
||||
element_type = RawElement::Type::kS4;
|
||||
element_family = ElementFamily::kShell;
|
||||
expected_field_count = 5U;
|
||||
} else if (EqualName(*type, "S4R")) {
|
||||
} else if (AsciiCaseInsensitiveEquals(*type, "S4R")) {
|
||||
element_type = RawElement::Type::kS4r;
|
||||
element_family = ElementFamily::kShell;
|
||||
expected_field_count = 5U;
|
||||
@@ -803,9 +778,7 @@ class MappingContext {
|
||||
return;
|
||||
}
|
||||
if (block.data[0].fields.size() == 2U) {
|
||||
std::int64_t ignored_integration_points = 0;
|
||||
if (!TryPositiveInteger(block.data[0].fields[1],
|
||||
ignored_integration_points)) {
|
||||
if (!ParsePositiveSourceLabel(block.data[0].fields[1]).HasValue()) {
|
||||
InputFailure("unsupported-shell-section-option", block.data[0].location,
|
||||
block.canonical_name, block.data[0].fields[1],
|
||||
"The optional shell integration-point field must be a "
|
||||
@@ -932,7 +905,7 @@ class MappingContext {
|
||||
if (element_set == nullptr || material == nullptr || section == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!EqualName(*section, "GENERAL")) {
|
||||
if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
|
||||
InputFailure("unsupported-section-formulation", block.location,
|
||||
block.canonical_name, *section,
|
||||
"Only SECTION=GENERAL belongs to the approved subset.");
|
||||
@@ -940,7 +913,8 @@ class MappingContext {
|
||||
}
|
||||
if (std::any_of(part.sections.begin(), part.sections.end(),
|
||||
[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,
|
||||
*element_set,
|
||||
@@ -1332,7 +1306,7 @@ class MappingContext {
|
||||
"NLGEOM requires NO in the approved subset.");
|
||||
return;
|
||||
}
|
||||
if (!EqualName(*nlgeom->value, "NO")) {
|
||||
if (!AsciiCaseInsensitiveEquals(*nlgeom->value, "NO")) {
|
||||
if (model_element_family_ == ElementFamily::kShell) {
|
||||
InputFailure("unsupported-nonlinear-geometry", block.location,
|
||||
block.canonical_name, *nlgeom->value,
|
||||
@@ -1510,22 +1484,24 @@ class MappingContext {
|
||||
|
||||
const RawPart* FindPart(const std::string& name) const {
|
||||
const auto found = std::find_if(
|
||||
parts_.begin(), parts_.end(),
|
||||
[&name](const RawPart& part) { return EqualName(part.name, name); });
|
||||
parts_.begin(), parts_.end(), [&name](const RawPart& part) {
|
||||
return AsciiCaseInsensitiveEquals(part.name, name);
|
||||
});
|
||||
return found == parts_.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
const RawInstance* FindInstance(const std::string& name) const {
|
||||
const auto found = std::find_if(instances_.begin(), instances_.end(),
|
||||
[&name](const RawInstance& instance) {
|
||||
return EqualName(instance.name, name);
|
||||
});
|
||||
const auto found =
|
||||
std::find_if(instances_.begin(), instances_.end(),
|
||||
[&name](const RawInstance& instance) {
|
||||
return AsciiCaseInsensitiveEquals(instance.name, name);
|
||||
});
|
||||
return found == instances_.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1534,9 +1510,10 @@ class MappingContext {
|
||||
|
||||
const RawSet* FindSet(const std::vector<RawSet>& sets,
|
||||
const std::string& name) const {
|
||||
const auto found = std::find_if(
|
||||
sets.begin(), sets.end(),
|
||||
[&name](const RawSet& set) { return EqualName(set.name, name); });
|
||||
const auto found =
|
||||
std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) {
|
||||
return AsciiCaseInsensitiveEquals(set.name, name);
|
||||
});
|
||||
return found == sets.end() ? nullptr : &*found;
|
||||
}
|
||||
|
||||
@@ -2000,7 +1977,8 @@ class MappingContext {
|
||||
const auto definition_instance = std::find_if(
|
||||
definition_.instances.begin(), definition_.instances.end(),
|
||||
[&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()) {
|
||||
InputFailure("unresolved-reference", raw_set.location,
|
||||
@@ -2010,11 +1988,11 @@ class MappingContext {
|
||||
}
|
||||
if (raw_set.is_node_set) {
|
||||
definition_.node_sets.erase(
|
||||
std::remove_if(definition_.node_sets.begin(),
|
||||
definition_.node_sets.end(),
|
||||
[&raw_set](const NodeSet& set) {
|
||||
return EqualName(set.name, raw_set.name);
|
||||
}),
|
||||
std::remove_if(
|
||||
definition_.node_sets.begin(), definition_.node_sets.end(),
|
||||
[&raw_set](const NodeSet& set) {
|
||||
return AsciiCaseInsensitiveEquals(set.name, raw_set.name);
|
||||
}),
|
||||
definition_.node_sets.end());
|
||||
NodeSet set{
|
||||
raw_set.name, definition_instance->name, {}, raw_set.location};
|
||||
@@ -2040,7 +2018,8 @@ class MappingContext {
|
||||
std::remove_if(definition_.element_sets.begin(),
|
||||
definition_.element_sets.end(),
|
||||
[&raw_set](const ElementSet& set) {
|
||||
return EqualName(set.name, raw_set.name);
|
||||
return AsciiCaseInsensitiveEquals(set.name,
|
||||
raw_set.name);
|
||||
}),
|
||||
definition_.element_sets.end());
|
||||
ElementSet set{
|
||||
@@ -2066,68 +2045,57 @@ class MappingContext {
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
||||
const std::string& target, const SourceLocation& location,
|
||||
const std::string& keyword) {
|
||||
std::vector<const NodeSet*> matching_sets;
|
||||
/// @brief Builds one stable node-target index from the completed candidate.
|
||||
SourceTargetIndex BuildSourceTargetIndex() const {
|
||||
std::vector<SourceTargetIndexEntry> entries;
|
||||
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) {
|
||||
if (EqualName(set.name, target)) {
|
||||
matching_sets.push_back(&set);
|
||||
for (const EntityIndex node : set.node_indices) {
|
||||
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::int64_t label = 0;
|
||||
if (TryPositiveInteger(target, label)) {
|
||||
for (std::size_t index = 0U; index < definition_.nodes.size(); ++index) {
|
||||
if (definition_.nodes[index].source_id.source_label == label) {
|
||||
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) {
|
||||
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
||||
const SourceTargetResolver& resolver, const std::string& target,
|
||||
const SourceLocation& location, const std::string& keyword) {
|
||||
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
|
||||
if (!resolved.HasValue()) {
|
||||
InputFailure(
|
||||
"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;
|
||||
}
|
||||
if (matching_nodes.size() > 1U) {
|
||||
InputFailure("unresolved-reference", location, keyword, target,
|
||||
"The direct node-label target is ambiguous across identity "
|
||||
"instances.");
|
||||
return std::nullopt;
|
||||
std::vector<EntityIndex> indices;
|
||||
indices.reserve(resolved.Value().size());
|
||||
for (const auto& entry : resolved.Value()) {
|
||||
indices.push_back(entry.entity_index);
|
||||
}
|
||||
if (matching_sets.size() == 1U && matching_nodes.size() == 1U) {
|
||||
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;
|
||||
return indices;
|
||||
}
|
||||
|
||||
void FinalizeStep() {
|
||||
std::vector<BoundaryCondition> boundaries = model_boundaries_;
|
||||
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
|
||||
step_.boundaries.end());
|
||||
const SourceTargetIndex target_index = BuildSourceTargetIndex();
|
||||
const SourceTargetResolver target_resolver{target_index};
|
||||
|
||||
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
|
||||
for (const auto& boundary : boundaries) {
|
||||
auto target =
|
||||
ResolveNodeTarget(boundary.target, boundary.location, "BOUNDARY");
|
||||
auto target = ResolveNodeTarget(target_resolver, boundary.target,
|
||||
boundary.location, "BOUNDARY");
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
@@ -2148,7 +2116,8 @@ class MappingContext {
|
||||
}
|
||||
}
|
||||
for (const auto& load : step_.loads) {
|
||||
if (!ResolveNodeTarget(load.target, load.location, "CLOAD")) {
|
||||
if (!ResolveNodeTarget(target_resolver, load.target, load.location,
|
||||
"CLOAD")) {
|
||||
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 <array>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/elements/euler_beam_3d.h"
|
||||
#include "fesa/elements/mitc4_shell.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
@@ -324,70 +322,29 @@ Status ValidateRecoveryInputs(const AnalysisModel& model,
|
||||
return Status::Ok();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Result<std::vector<EntityIndex>> ResolveLoadTarget(const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
std::vector<const NodeSet*> sets;
|
||||
for (const auto& set : domain.NodeSets()) {
|
||||
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())) {
|
||||
Result<std::vector<EntityIndex>> ResolveLoadTarget(
|
||||
const SourceTargetResolver& resolver, const Domain& domain,
|
||||
const NodalLoad& load) {
|
||||
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", load.target});
|
||||
if (!resolved.HasValue()) {
|
||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
||||
"invalid-node-station-entity", load.location, load.target,
|
||||
"A station-eligibility load target must resolve unambiguously.");
|
||||
}
|
||||
if (!sets.empty()) {
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const EntityIndex node : sets.front()->node_indices) {
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
||||
"invalid-node-station-entity", load.location, load.target,
|
||||
"A station-eligibility node set must contain unique valid nodes.");
|
||||
}
|
||||
seen[node] = 1U;
|
||||
std::vector<EntityIndex> nodes;
|
||||
nodes.reserve(resolved.Value().size());
|
||||
std::vector<unsigned char> seen(domain.Nodes().size(), 0U);
|
||||
for (const auto& target : resolved.Value()) {
|
||||
const EntityIndex node = target.entity_index;
|
||||
if (node >= domain.Nodes().size() || seen[node] != 0U) {
|
||||
return RecoveryResultFailure<std::vector<EntityIndex>>(
|
||||
"invalid-node-station-entity", load.location, load.target,
|
||||
"A station-eligibility node set must contain unique valid nodes.");
|
||||
}
|
||||
return Result<std::vector<EntityIndex>>::Success(
|
||||
sets.front()->node_indices);
|
||||
seen[node] = 1U;
|
||||
nodes.push_back(node);
|
||||
}
|
||||
if (!nodes.empty()) {
|
||||
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.");
|
||||
return Result<std::vector<EntityIndex>>::Success(std::move(nodes));
|
||||
}
|
||||
|
||||
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,
|
||||
"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) {
|
||||
const EntityIndex load_index = model.ActiveLoads()[order];
|
||||
if (load_index != order || load_index >= model.Step().loads.size()) {
|
||||
@@ -893,7 +852,7 @@ ResultRecovery::NormalizeSectionResultantsToNodeStations(
|
||||
"nonfinite-node-station-value", load.location, load.target,
|
||||
"Station eligibility requires finite concentrated loads.");
|
||||
}
|
||||
auto targets = ResolveLoadTarget(domain, load);
|
||||
auto targets = ResolveLoadTarget(target_resolver, domain, load);
|
||||
if (!targets.HasValue()) {
|
||||
return Result<std::vector<NodeStationResultRow>>::Failure(
|
||||
targets.GetStatus());
|
||||
|
||||
Reference in New Issue
Block a user