feat(shell-surface-pressure-load): step 1 - map-pressure-input-contract
This commit is contained in:
@@ -33,6 +33,7 @@ add_library(
|
||||
io/abaqus/input_reader.cpp
|
||||
io/abaqus/material_property_mapper.cpp
|
||||
io/abaqus/step_definition_mapper.cpp
|
||||
io/abaqus/surface_definition_mapper.cpp
|
||||
io/abaqus/topology_mapper.cpp
|
||||
io/hdf5/hdf5_atomic_file.cpp
|
||||
io/hdf5/hdf5_model_writer.cpp
|
||||
|
||||
@@ -65,6 +65,11 @@ const std::vector<EntityIndex>& AnalysisModel::ActiveLoads() const noexcept {
|
||||
return active_loads_;
|
||||
}
|
||||
|
||||
const std::vector<SurfacePressureLoadDefinition>&
|
||||
AnalysisModel::ActiveSurfacePressureLoads() const noexcept {
|
||||
return Step().SurfacePressureLoads();
|
||||
}
|
||||
|
||||
AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
|
||||
std::vector<bool> reachable_materials(domain.Materials().Size(), false);
|
||||
std::vector<bool> reachable_properties(domain.Properties().Size(), false);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "io/abaqus/domain_mapping_context.h"
|
||||
#include "io/abaqus/material_property_mapper.h"
|
||||
#include "io/abaqus/step_definition_mapper.h"
|
||||
#include "io/abaqus/surface_definition_mapper.h"
|
||||
#include "io/abaqus/topology_mapper.h"
|
||||
|
||||
namespace fesa {
|
||||
@@ -26,6 +27,10 @@ Result<Domain> AbaqusDomainMapper::Map(const ParsedInput& input) const {
|
||||
if (!status.IsOk()) {
|
||||
return Result<Domain>::Failure(std::move(status));
|
||||
}
|
||||
status = abaqus_internal::SurfaceDefinitionMapper{}.Map(context);
|
||||
if (!status.IsOk()) {
|
||||
return Result<Domain>::Failure(std::move(status));
|
||||
}
|
||||
status = abaqus_internal::StepDefinitionMapper{}.Map(context);
|
||||
if (!status.IsOk()) {
|
||||
return Result<Domain>::Failure(std::move(status));
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
@@ -29,6 +30,15 @@ std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
std::string CanonicalizeAscii(std::string value) {
|
||||
for (char& character : value) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
character = static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
struct RawNode {
|
||||
std::int64_t label;
|
||||
std::string label_text;
|
||||
@@ -95,6 +105,30 @@ struct RawAssemblySet {
|
||||
SourceLocation location;
|
||||
};
|
||||
|
||||
struct RawSurfaceMember {
|
||||
std::string target;
|
||||
ShellSurfaceSide side;
|
||||
SourceLocation location;
|
||||
std::size_t ordinal;
|
||||
};
|
||||
|
||||
struct RawElementSurface {
|
||||
std::string name;
|
||||
SourceLocation location;
|
||||
std::size_t declaration_ordinal;
|
||||
std::vector<RawSurfaceMember> members;
|
||||
};
|
||||
|
||||
struct RawSurfacePressureLoad {
|
||||
std::string surface_name;
|
||||
double magnitude;
|
||||
std::string magnitude_raw;
|
||||
SourceLocation keyword_location;
|
||||
SourceLocation data_location;
|
||||
std::size_t data_ordinal;
|
||||
std::size_t load_ordinal;
|
||||
};
|
||||
|
||||
struct RawMaterial {
|
||||
std::string name;
|
||||
double youngs_modulus{0.0};
|
||||
@@ -106,6 +140,8 @@ struct RawMaterial {
|
||||
|
||||
struct RawStep {
|
||||
SourceLocation location;
|
||||
std::string name_raw;
|
||||
std::string name_canonical;
|
||||
bool has_static{false};
|
||||
StaticProcedureKind procedure_kind{
|
||||
StaticProcedureKind::kLinearSmallDisplacement};
|
||||
@@ -113,6 +149,8 @@ struct RawStep {
|
||||
std::array<double, 4> static_values{};
|
||||
std::vector<PrescribedDisplacementDefinition> boundaries;
|
||||
std::vector<NodalLoad> loads;
|
||||
std::vector<RawSurfacePressureLoad> surface_pressure_loads;
|
||||
std::size_t next_load_ordinal{0U};
|
||||
};
|
||||
|
||||
struct MappingFailure {
|
||||
@@ -173,6 +211,13 @@ class DomainMappingContext::Impl {
|
||||
return CurrentStatus();
|
||||
}
|
||||
|
||||
Status FinalizeElementSurfaces() {
|
||||
if (!failure_) {
|
||||
ResolveElementSurfaces();
|
||||
}
|
||||
return CurrentStatus();
|
||||
}
|
||||
|
||||
Status FinalizeStepCandidate() {
|
||||
if (!failure_) {
|
||||
FinalizeStep();
|
||||
@@ -346,6 +391,55 @@ class DomainMappingContext::Impl {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ParseSurfacePressureDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword) {
|
||||
std::size_t cursor = 0U;
|
||||
if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) {
|
||||
++cursor;
|
||||
}
|
||||
const std::size_t integer_start = cursor;
|
||||
while (cursor < text.size() && text[cursor] >= '0' &&
|
||||
text[cursor] <= '9') {
|
||||
++cursor;
|
||||
}
|
||||
const bool has_integer_digits = cursor != integer_start;
|
||||
bool has_fraction_digits = false;
|
||||
if (cursor < text.size() && text[cursor] == '.') {
|
||||
++cursor;
|
||||
const std::size_t fraction_start = cursor;
|
||||
while (cursor < text.size() && text[cursor] >= '0' &&
|
||||
text[cursor] <= '9') {
|
||||
++cursor;
|
||||
}
|
||||
has_fraction_digits = cursor != fraction_start;
|
||||
}
|
||||
if (!has_integer_digits && !has_fraction_digits) {
|
||||
return InputFailure("invalid-numeric-value", location, keyword, text,
|
||||
"Pressure values require C-locale decimal syntax.");
|
||||
}
|
||||
if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) {
|
||||
++cursor;
|
||||
if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) {
|
||||
++cursor;
|
||||
}
|
||||
const std::size_t exponent_start = cursor;
|
||||
while (cursor < text.size() && text[cursor] >= '0' &&
|
||||
text[cursor] <= '9') {
|
||||
++cursor;
|
||||
}
|
||||
if (cursor == exponent_start) {
|
||||
return InputFailure("invalid-numeric-value", location, keyword, text,
|
||||
"Pressure exponents require one or more digits.");
|
||||
}
|
||||
}
|
||||
if (cursor != text.size()) {
|
||||
return InputFailure("invalid-numeric-value", location, keyword, text,
|
||||
"Pressure values require C-locale decimal syntax.");
|
||||
}
|
||||
return ParseDouble(text, value, location, keyword);
|
||||
}
|
||||
|
||||
bool ParseNonlinearStaticDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword) {
|
||||
@@ -430,6 +524,14 @@ class DomainMappingContext::Impl {
|
||||
});
|
||||
}
|
||||
|
||||
bool ContainsSurfaceName(const std::string& name) const {
|
||||
return std::any_of(
|
||||
element_surfaces_.begin(), element_surfaces_.end(),
|
||||
[&name](const RawElementSurface& surface) {
|
||||
return AsciiCaseInsensitiveEquals(surface.name, name);
|
||||
});
|
||||
}
|
||||
|
||||
void ParseBlocks() {
|
||||
definition_.source_path = input_.source_path;
|
||||
definition_.source_content_identity = input_.source_content_identity;
|
||||
@@ -1159,6 +1261,13 @@ class DomainMappingContext::Impl {
|
||||
}
|
||||
assembly_set_seen_ = true;
|
||||
ParseAssemblySet(block, false);
|
||||
} else if (block.canonical_name == "SURFACE") {
|
||||
if (instances_.empty()) {
|
||||
InvalidKeywordLocation(
|
||||
block, "Assembly surfaces follow at least one identity instance.");
|
||||
return;
|
||||
}
|
||||
ParseElementSurface(block);
|
||||
} else {
|
||||
RejectUnknown(block);
|
||||
}
|
||||
@@ -1217,7 +1326,11 @@ class DomainMappingContext::Impl {
|
||||
|
||||
void ParseAssemblySet(const KeywordBlock& block, bool node_set) {
|
||||
const std::string_view name_parameter = node_set ? "NSET" : "ELSET";
|
||||
if (!ValidateParameters(block, {name_parameter, "INSTANCE", "GENERATE"})) {
|
||||
const std::vector<std::string_view> allowed_parameters =
|
||||
node_set ? std::vector<std::string_view>{"NSET", "INSTANCE", "GENERATE"}
|
||||
: std::vector<std::string_view>{"ELSET", "INSTANCE", "GENERATE",
|
||||
"INTERNAL"};
|
||||
if (!ValidateParameters(block, allowed_parameters)) {
|
||||
return;
|
||||
}
|
||||
const auto* name = RequiredParameterValue(block, name_parameter);
|
||||
@@ -1232,6 +1345,13 @@ class DomainMappingContext::Impl {
|
||||
"GENERATE is a valueless flag.");
|
||||
return;
|
||||
}
|
||||
const auto* internal_parameter = Parameter(block, "INTERNAL");
|
||||
if (internal_parameter != nullptr && internal_parameter->value) {
|
||||
InputFailure("invalid-keyword-parameter", block.location,
|
||||
block.canonical_name, "INTERNAL",
|
||||
"INTERNAL is a valueless result-membership flag.");
|
||||
return;
|
||||
}
|
||||
if (ContainsAssemblySetName(node_set, *name)) {
|
||||
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
||||
*name,
|
||||
@@ -1246,6 +1366,59 @@ class DomainMappingContext::Impl {
|
||||
assembly_sets_.push_back(std::move(set));
|
||||
}
|
||||
|
||||
void ParseElementSurface(const KeywordBlock& block) {
|
||||
if (!ValidateParameters(block, {"TYPE", "NAME"})) {
|
||||
return;
|
||||
}
|
||||
const auto* type = RequiredParameterValue(block, "TYPE");
|
||||
const auto* name = RequiredParameterValue(block, "NAME");
|
||||
if (type == nullptr || name == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) {
|
||||
InputFailure("unsupported-surface-type", block.location,
|
||||
block.canonical_name, *type,
|
||||
"Only TYPE=ELEMENT belongs to the approved surface subset.");
|
||||
return;
|
||||
}
|
||||
if (ContainsSurfaceName(*name)) {
|
||||
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
||||
*name, "Surface names are unique in the assembly namespace.");
|
||||
return;
|
||||
}
|
||||
if (block.data.empty()) {
|
||||
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
||||
*name, "An element surface requires at least one member.");
|
||||
return;
|
||||
}
|
||||
|
||||
RawElementSurface surface{*name, block.location, element_surfaces_.size(),
|
||||
{}};
|
||||
for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) {
|
||||
const auto& row = block.data[ordinal];
|
||||
if (row.fields.size() != 2U || row.fields[0].empty() ||
|
||||
row.fields[1].empty()) {
|
||||
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
||||
*name,
|
||||
"Surface rows require one member and SPOS or SNEG.");
|
||||
return;
|
||||
}
|
||||
ShellSurfaceSide side = ShellSurfaceSide::kSpos;
|
||||
if (AsciiCaseInsensitiveEquals(row.fields[1], "SPOS")) {
|
||||
side = ShellSurfaceSide::kSpos;
|
||||
} else if (AsciiCaseInsensitiveEquals(row.fields[1], "SNEG")) {
|
||||
side = ShellSurfaceSide::kSneg;
|
||||
} else {
|
||||
InputFailure("unsupported-surface-side", row.location,
|
||||
block.canonical_name, row.fields[1],
|
||||
"Surface sides must be SPOS or SNEG.");
|
||||
return;
|
||||
}
|
||||
surface.members.push_back({row.fields[0], side, row.location, ordinal});
|
||||
}
|
||||
element_surfaces_.push_back(std::move(surface));
|
||||
}
|
||||
|
||||
void ParseMaterial(const KeywordBlock& block) {
|
||||
if (!assembly_seen_ || model_boundary_seen_) {
|
||||
InvalidKeywordLocation(block,
|
||||
@@ -1389,6 +1562,39 @@ class DomainMappingContext::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
void ParseDsload(const KeywordBlock& block) {
|
||||
if (!ValidateParameters(block, {}) || block.data.empty()) {
|
||||
if (!failure_) {
|
||||
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
||||
"", "DSLOAD requires one or more rows.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) {
|
||||
const auto& row = block.data[ordinal];
|
||||
if (row.fields.size() != 3U || row.fields[0].empty() ||
|
||||
row.fields[1].empty() || row.fields[2].empty()) {
|
||||
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
||||
"", "DSLOAD rows require surface, P, magnitude.");
|
||||
return;
|
||||
}
|
||||
if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) {
|
||||
InputFailure("unsupported-load-type", row.location,
|
||||
block.canonical_name, row.fields[1],
|
||||
"Only uniform surface pressure load type P is supported.");
|
||||
return;
|
||||
}
|
||||
double magnitude = 0.0;
|
||||
if (!ParseSurfacePressureDouble(row.fields[2], magnitude, row.location,
|
||||
block.canonical_name)) {
|
||||
return;
|
||||
}
|
||||
step_.surface_pressure_loads.push_back(
|
||||
{row.fields[0], magnitude, row.fields[2], block.location,
|
||||
row.location, ordinal, step_.next_load_ordinal++});
|
||||
}
|
||||
}
|
||||
|
||||
void ParseStepStart(const KeywordBlock& block) {
|
||||
if (step_seen_) {
|
||||
const auto* nlgeom = Parameter(block, "NLGEOM");
|
||||
@@ -1444,10 +1650,21 @@ class DomainMappingContext::Impl {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const auto* name = Parameter(block, "NAME");
|
||||
if (name != nullptr && (!name->value || name->value->empty())) {
|
||||
InputFailure("invalid-keyword-parameter", block.location,
|
||||
block.canonical_name, "NAME",
|
||||
"STEP NAME must not be empty when it is present.");
|
||||
return;
|
||||
}
|
||||
step_seen_ = true;
|
||||
in_step_ = true;
|
||||
step_ = RawStep{block.location, false, procedure_kind, nlgeom, {}, {},
|
||||
{}};
|
||||
step_ = {};
|
||||
step_.location = block.location;
|
||||
step_.name_raw = name == nullptr ? "Step-1" : *name->value;
|
||||
step_.name_canonical = CanonicalizeAscii(step_.name_raw);
|
||||
step_.procedure_kind = procedure_kind;
|
||||
step_.nlgeom = nlgeom;
|
||||
step_load_seen_ = false;
|
||||
step_no_op_seen_ = false;
|
||||
}
|
||||
@@ -1476,6 +1693,7 @@ class DomainMappingContext::Impl {
|
||||
if (block.canonical_name == "STATIC") {
|
||||
active_output_ = false;
|
||||
if (!step_.boundaries.empty() || !step_.loads.empty() ||
|
||||
!step_.surface_pressure_loads.empty() ||
|
||||
step_no_op_seen_) {
|
||||
InvalidKeywordLocation(
|
||||
block, "STATIC must be the first keyword in the sole step.");
|
||||
@@ -1501,6 +1719,19 @@ class DomainMappingContext::Impl {
|
||||
return;
|
||||
}
|
||||
ParseCload(block, step_.loads);
|
||||
if (!failure_) {
|
||||
step_.next_load_ordinal += block.data.size();
|
||||
}
|
||||
step_load_seen_ = !failure_;
|
||||
} else if (block.canonical_name == "DSLOAD") {
|
||||
active_output_ = false;
|
||||
if (!step_.has_static || step_no_op_seen_) {
|
||||
InvalidKeywordLocation(
|
||||
block,
|
||||
"DSLOAD follows STATIC and boundaries and precedes no-op requests.");
|
||||
return;
|
||||
}
|
||||
ParseDsload(block);
|
||||
step_load_seen_ = !failure_;
|
||||
} else if (block.canonical_name == "RESTART") {
|
||||
active_output_ = false;
|
||||
@@ -2180,6 +2411,221 @@ class DomainMappingContext::Impl {
|
||||
}
|
||||
}
|
||||
|
||||
bool AppendSurfaceFacet(
|
||||
const RawSurfaceMember& member, const RawInstance& raw_instance,
|
||||
std::size_t instance_ordinal,
|
||||
EntityIndex element_index, SurfaceMemberKind member_kind,
|
||||
const std::string& elset_name_raw,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates) {
|
||||
if (static_cast<std::size_t>(element_index) >=
|
||||
definition_.shell_elements.size()) {
|
||||
return InputFailure(
|
||||
"unsupported-element-formulation", member.location, "SURFACE",
|
||||
member.target,
|
||||
"Element surfaces may resolve only to S4 or S4R shell elements.");
|
||||
}
|
||||
const RawPart* part = FindPart(raw_instance.part_name);
|
||||
if (part == nullptr) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target,
|
||||
"The surface instance must resolve to its source part.");
|
||||
}
|
||||
const auto& element = definition_.shell_elements[element_index];
|
||||
const std::string source_type =
|
||||
element.source_type == ShellSourceElementType::kS4 ? "S4" : "S4R";
|
||||
ElementSurfaceFacetDefinition facet{
|
||||
element_index,
|
||||
0U,
|
||||
part->name,
|
||||
CanonicalizeAscii(part->name),
|
||||
raw_instance.name,
|
||||
CanonicalizeAscii(raw_instance.name),
|
||||
element.source_id.source_label,
|
||||
element.source_id.source_label_text,
|
||||
source_type,
|
||||
member.side,
|
||||
member.location,
|
||||
member.ordinal,
|
||||
member_kind,
|
||||
elset_name_raw,
|
||||
member_kind == SurfaceMemberKind::kElset
|
||||
? CanonicalizeAscii(elset_name_raw)
|
||||
: ""};
|
||||
candidates->push_back({std::move(facet), instance_ordinal});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ResolveDirectSurfaceMember(
|
||||
const RawSurfaceMember& member,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates) {
|
||||
const std::size_t dot = member.target.find('.');
|
||||
if (dot == std::string::npos || dot == 0U ||
|
||||
dot + 1U >= member.target.size() ||
|
||||
member.target.find('.', dot + 1U) != std::string::npos) {
|
||||
return InputFailure(
|
||||
"unresolved-reference", member.location, "SURFACE", member.target,
|
||||
"A direct surface member must be instance.positive-element-label.");
|
||||
}
|
||||
const std::string instance_name = member.target.substr(0U, dot);
|
||||
std::int64_t source_label = 0;
|
||||
if (!ParseInteger(member.target.substr(dot + 1U), source_label,
|
||||
member.location, "SURFACE", true)) {
|
||||
return false;
|
||||
}
|
||||
const auto raw_instance = std::find_if(
|
||||
instances_.begin(), instances_.end(), [&instance_name](const RawInstance& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, instance_name);
|
||||
});
|
||||
if (raw_instance == instances_.end()) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target,
|
||||
"A direct surface member instance must resolve.");
|
||||
}
|
||||
const auto element = std::find_if(
|
||||
definition_.shell_elements.begin(), definition_.shell_elements.end(),
|
||||
[&raw_instance, source_label](const Mitc4ShellDefinition& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.source_id.instance_name,
|
||||
raw_instance->name) &&
|
||||
value.source_id.source_label == source_label;
|
||||
});
|
||||
if (element == definition_.shell_elements.end()) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target,
|
||||
"A direct surface member must resolve to an S4 or S4R.");
|
||||
}
|
||||
return AppendSurfaceFacet(
|
||||
member, *raw_instance,
|
||||
static_cast<std::size_t>(std::distance(instances_.begin(), raw_instance)),
|
||||
static_cast<EntityIndex>(
|
||||
std::distance(definition_.shell_elements.begin(), element)),
|
||||
SurfaceMemberKind::kDirect, "", candidates);
|
||||
}
|
||||
|
||||
bool ResolveElsetSurfaceMember(
|
||||
const RawSurfaceMember& member,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates) {
|
||||
const auto raw_set = std::find_if(
|
||||
assembly_sets_.begin(), assembly_sets_.end(), [&member](const RawAssemblySet& value) {
|
||||
return !value.is_node_set &&
|
||||
AsciiCaseInsensitiveEquals(value.name, member.target);
|
||||
});
|
||||
if (raw_set == assembly_sets_.end()) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target,
|
||||
"A surface ELSET must be assembly-scoped and explicit-instance.");
|
||||
}
|
||||
const auto raw_instance = std::find_if(
|
||||
instances_.begin(), instances_.end(), [&raw_set](const RawInstance& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name);
|
||||
});
|
||||
if (raw_instance == instances_.end()) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target, "The surface ELSET instance must resolve.");
|
||||
}
|
||||
const auto element_set = std::find_if(
|
||||
definition_.element_sets.begin(), definition_.element_sets.end(),
|
||||
[&raw_set](const ElementSet& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, raw_set->name) &&
|
||||
value.instance_name &&
|
||||
AsciiCaseInsensitiveEquals(*value.instance_name,
|
||||
raw_set->instance_name);
|
||||
});
|
||||
if (element_set == definition_.element_sets.end()) {
|
||||
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
||||
member.target,
|
||||
"The surface ELSET must expand in its explicit instance.");
|
||||
}
|
||||
for (const EntityIndex element_index : element_set->element_indices) {
|
||||
if (!AppendSurfaceFacet(
|
||||
member, *raw_instance,
|
||||
static_cast<std::size_t>(
|
||||
std::distance(instances_.begin(), raw_instance)),
|
||||
element_index, SurfaceMemberKind::kElset, member.target,
|
||||
candidates)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ResolveElementSurfaces() {
|
||||
std::vector<ElementSurfaceDefinition> resolved_surfaces;
|
||||
resolved_surfaces.reserve(element_surfaces_.size());
|
||||
for (const auto& surface : element_surfaces_) {
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>
|
||||
candidates;
|
||||
for (const auto& member : surface.members) {
|
||||
const bool is_direct = member.target.find('.') != std::string::npos;
|
||||
if (!(is_direct
|
||||
? ResolveDirectSurfaceMember(member, &candidates)
|
||||
: ResolveElsetSurfaceMember(member, &candidates))) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (candidates.empty()) {
|
||||
InputFailure("empty-surface", surface.location, "SURFACE", surface.name,
|
||||
"An element surface must resolve at least one facet.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>
|
||||
unique_facets;
|
||||
for (auto& candidate : candidates) {
|
||||
const auto same_element = std::find_if(
|
||||
unique_facets.begin(), unique_facets.end(),
|
||||
[&candidate](const auto& existing) {
|
||||
return existing.first.instance_name_canonical ==
|
||||
candidate.first.instance_name_canonical &&
|
||||
existing.first.source_element_label ==
|
||||
candidate.first.source_element_label;
|
||||
});
|
||||
if (same_element != unique_facets.end()) {
|
||||
if (same_element->first.side != candidate.first.side) {
|
||||
InputFailure("conflicting-surface-membership",
|
||||
candidate.first.first_member_location, "SURFACE",
|
||||
surface.name,
|
||||
"One shell element cannot appear on both surface sides.");
|
||||
return;
|
||||
}
|
||||
definition_.warnings.push_back(
|
||||
{Severity::kWarning,
|
||||
"duplicate-surface-member-normalized",
|
||||
candidate.first.first_member_location,
|
||||
"SURFACE",
|
||||
surface.name,
|
||||
"A repeated same-side surface member was normalized once."});
|
||||
continue;
|
||||
}
|
||||
unique_facets.push_back(std::move(candidate));
|
||||
}
|
||||
std::sort(unique_facets.begin(), unique_facets.end(),
|
||||
[](const auto& lhs, const auto& rhs) {
|
||||
const int lhs_side =
|
||||
lhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1;
|
||||
const int rhs_side =
|
||||
rhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1;
|
||||
return std::tie(lhs.second, lhs.first.source_element_label,
|
||||
lhs_side) <
|
||||
std::tie(rhs.second, rhs.first.source_element_label,
|
||||
rhs_side);
|
||||
});
|
||||
|
||||
ElementSurfaceDefinition resolved{surface.name, CanonicalizeAscii(surface.name),
|
||||
{}, surface.location,
|
||||
surface.declaration_ordinal};
|
||||
resolved.facets.reserve(unique_facets.size());
|
||||
for (std::size_t ordinal = 0U; ordinal < unique_facets.size(); ++ordinal) {
|
||||
unique_facets[ordinal].first.facet_ordinal = ordinal;
|
||||
resolved.facets.push_back(std::move(unique_facets[ordinal].first));
|
||||
}
|
||||
resolved_surfaces.push_back(std::move(resolved));
|
||||
}
|
||||
definition_.element_surfaces = std::move(resolved_surfaces);
|
||||
}
|
||||
|
||||
/// @brief Builds one stable node-target index from the completed candidate.
|
||||
SourceTargetIndex BuildSourceTargetIndex() const {
|
||||
std::vector<SourceTargetIndexEntry> entries;
|
||||
@@ -2257,13 +2703,44 @@ class DomainMappingContext::Impl {
|
||||
return;
|
||||
}
|
||||
}
|
||||
std::vector<SurfacePressureLoadDefinition> surface_pressure_loads;
|
||||
surface_pressure_loads.reserve(step_.surface_pressure_loads.size());
|
||||
for (const auto& load : step_.surface_pressure_loads) {
|
||||
const auto surface = std::find_if(
|
||||
definition_.element_surfaces.begin(),
|
||||
definition_.element_surfaces.end(), [&load](const auto& candidate) {
|
||||
return AsciiCaseInsensitiveEquals(candidate.surface_name_raw,
|
||||
load.surface_name);
|
||||
});
|
||||
if (surface == definition_.element_surfaces.end()) {
|
||||
InputFailure("unresolved-reference", load.data_location, "DSLOAD",
|
||||
load.surface_name,
|
||||
"A DSLOAD surface must resolve after surface validation.");
|
||||
return;
|
||||
}
|
||||
surface_pressure_loads.push_back(
|
||||
{step_.name_raw,
|
||||
step_.name_canonical,
|
||||
static_cast<EntityIndex>(
|
||||
std::distance(definition_.element_surfaces.begin(), surface)),
|
||||
load.surface_name,
|
||||
CanonicalizeAscii(load.surface_name),
|
||||
"P",
|
||||
load.magnitude,
|
||||
load.magnitude_raw,
|
||||
load.keyword_location,
|
||||
load.data_location,
|
||||
load.data_ordinal,
|
||||
load.load_ordinal});
|
||||
}
|
||||
|
||||
// Static time-control values are provenance only: V0 creates exactly
|
||||
// the canonical final frame 0 and performs no increment loop here.
|
||||
definition_.steps.push_back(
|
||||
{"Step-1", std::move(boundaries), step_.loads, step_.procedure_kind,
|
||||
step_.nlgeom, step_.static_values[0], step_.static_values[1],
|
||||
step_.static_values[2], step_.static_values[3], step_.location});
|
||||
{"Step-1", std::move(boundaries), step_.loads,
|
||||
std::move(surface_pressure_loads), step_.procedure_kind, step_.nlgeom,
|
||||
step_.static_values[0], step_.static_values[1], step_.static_values[2],
|
||||
step_.static_values[3], step_.location});
|
||||
}
|
||||
|
||||
const ParsedInput& input_;
|
||||
@@ -2273,6 +2750,7 @@ class DomainMappingContext::Impl {
|
||||
std::vector<RawPart> parts_;
|
||||
std::vector<RawInstance> instances_;
|
||||
std::vector<RawAssemblySet> assembly_sets_;
|
||||
std::vector<RawElementSurface> element_surfaces_;
|
||||
std::vector<RawMaterial> materials_;
|
||||
std::vector<PrescribedDisplacementDefinition> model_boundaries_;
|
||||
RawStep step_{};
|
||||
@@ -2322,6 +2800,10 @@ Status DomainMappingContext::FinalizeTopology() {
|
||||
return impl_->FinalizeTopology();
|
||||
}
|
||||
|
||||
Status DomainMappingContext::FinalizeElementSurfaces() {
|
||||
return impl_->FinalizeElementSurfaces();
|
||||
}
|
||||
|
||||
Status DomainMappingContext::FinalizeStep() {
|
||||
return impl_->FinalizeStepCandidate();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace fesa::abaqus_internal {
|
||||
|
||||
class DomainBuilder;
|
||||
class MaterialPropertyMapper;
|
||||
class SurfaceDefinitionMapper;
|
||||
class StepDefinitionMapper;
|
||||
class TopologyMapper;
|
||||
|
||||
@@ -53,6 +54,7 @@ class DomainMappingContext {
|
||||
|
||||
friend class DomainBuilder;
|
||||
friend class MaterialPropertyMapper;
|
||||
friend class SurfaceDefinitionMapper;
|
||||
friend class StepDefinitionMapper;
|
||||
friend class TopologyMapper;
|
||||
|
||||
@@ -65,6 +67,9 @@ class DomainMappingContext {
|
||||
/// @brief Expands identity instances and stable topology candidates.
|
||||
Status FinalizeTopology();
|
||||
|
||||
/// @brief Resolves assembly-level element surfaces after topology expansion.
|
||||
Status FinalizeElementSurfaces();
|
||||
|
||||
/// @brief Resolves the sole static step against finalized source targets.
|
||||
Status FinalizeStep();
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ bool IsAsciiWhitespace(char value) noexcept {
|
||||
value == '\f' || value == '\v';
|
||||
}
|
||||
|
||||
bool IsAllowedInputCharacter(unsigned char value) noexcept {
|
||||
return value == '\t' || (value >= 0x20U && value <= 0x7eU);
|
||||
}
|
||||
|
||||
std::string Trim(std::string_view text) {
|
||||
while (!text.empty() && IsAsciiWhitespace(text.front())) {
|
||||
text.remove_prefix(1U);
|
||||
@@ -61,6 +65,44 @@ std::vector<std::string> SplitFields(std::string_view line) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
bool IsKeywordContinuation(std::string_view trimmed_line) noexcept {
|
||||
return !trimmed_line.empty() && trimmed_line.back() == ',';
|
||||
}
|
||||
|
||||
bool AppendKeywordParameters(KeywordBlock& block,
|
||||
const std::vector<std::string>& fields,
|
||||
std::size_t first_field,
|
||||
bool has_continuation) {
|
||||
if (has_continuation && fields.empty()) {
|
||||
return false;
|
||||
}
|
||||
const std::size_t field_count =
|
||||
has_continuation ? fields.size() - 1U : fields.size();
|
||||
if (field_count < first_field) {
|
||||
return false;
|
||||
}
|
||||
for (std::size_t index = first_field; index < field_count; ++index) {
|
||||
const std::string& field = fields[index];
|
||||
if (field.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string parameter_name =
|
||||
Trim(std::string_view{field}.substr(0U, equals));
|
||||
if (parameter_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeywordParameter parameter{UppercaseAscii(parameter_name), std::nullopt};
|
||||
if (equals != std::string::npos) {
|
||||
parameter.value = Trim(std::string_view{field}.substr(equals + 1U));
|
||||
}
|
||||
block.parameters.push_back(std::move(parameter));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// @brief Computes the stable identity of the exact source bytes.
|
||||
/// @note This runs before line-ending handling so parser provenance is not
|
||||
/// affected by text normalization.
|
||||
@@ -112,7 +154,13 @@ Result<ParsedInput> AbaqusInputReader::Read(
|
||||
"The Abaqus input file could not be read completely.");
|
||||
}
|
||||
|
||||
ParsedInput parsed{source_path, ContentIdentity(bytes), {}};
|
||||
struct PhysicalLine {
|
||||
std::string original_line;
|
||||
std::string trimmed_line;
|
||||
std::size_t line_number;
|
||||
};
|
||||
|
||||
std::vector<PhysicalLine> physical_lines;
|
||||
std::size_t line_start = 0U;
|
||||
std::size_t line_number = 1U;
|
||||
while (line_start < bytes.size()) {
|
||||
@@ -123,60 +171,20 @@ Result<ParsedInput> AbaqusInputReader::Read(
|
||||
if (!original_line.empty() && original_line.back() == '\r') {
|
||||
original_line.pop_back();
|
||||
}
|
||||
|
||||
const std::string trimmed_line = Trim(original_line);
|
||||
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) {
|
||||
if (trimmed_line.front() == '*') {
|
||||
const auto fields = SplitFields(trimmed_line);
|
||||
const std::string keyword_text =
|
||||
fields.empty() ? std::string{}
|
||||
: Trim(std::string_view{fields[0]}.substr(1U));
|
||||
if (keyword_text.empty()) {
|
||||
return Failure(source_path, line_number, "malformed-keyword",
|
||||
trimmed_line,
|
||||
"A keyword line requires a non-empty keyword name.");
|
||||
}
|
||||
|
||||
KeywordBlock block{UppercaseAscii(keyword_text),
|
||||
original_line,
|
||||
{},
|
||||
{},
|
||||
{source_path, line_number}};
|
||||
for (std::size_t index = 1U; index < fields.size(); ++index) {
|
||||
const std::string& field = fields[index];
|
||||
if (field.empty()) {
|
||||
return Failure(source_path, line_number, "malformed-keyword",
|
||||
block.canonical_name,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string parameter_name =
|
||||
Trim(std::string_view{field}.substr(0U, equals));
|
||||
if (parameter_name.empty()) {
|
||||
return Failure(source_path, line_number, "malformed-keyword",
|
||||
block.canonical_name,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
KeywordParameter parameter{UppercaseAscii(parameter_name),
|
||||
std::nullopt};
|
||||
if (equals != std::string::npos) {
|
||||
parameter.value = Trim(std::string_view{field}.substr(equals + 1U));
|
||||
}
|
||||
block.parameters.push_back(std::move(parameter));
|
||||
}
|
||||
parsed.blocks.push_back(std::move(block));
|
||||
} else {
|
||||
if (parsed.blocks.empty()) {
|
||||
return Failure(source_path, line_number, "orphan-data-line", "",
|
||||
"A data line must follow a keyword line.");
|
||||
}
|
||||
parsed.blocks.back().data.push_back(
|
||||
{SplitFields(original_line), {source_path, line_number}});
|
||||
if (original_line.size() > 256U) {
|
||||
return Failure(source_path, line_number, "input-line-too-long", "",
|
||||
"Each physical input line is limited to 256 ASCII bytes.");
|
||||
}
|
||||
for (const unsigned char character : original_line) {
|
||||
if (!IsAllowedInputCharacter(character)) {
|
||||
return Failure(source_path, line_number, "invalid-input-character", "",
|
||||
"Input lines must use printable 7-bit ASCII or tabs.");
|
||||
}
|
||||
}
|
||||
|
||||
physical_lines.push_back(
|
||||
{original_line, Trim(original_line), line_number});
|
||||
|
||||
if (newline == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
@@ -184,6 +192,75 @@ Result<ParsedInput> AbaqusInputReader::Read(
|
||||
++line_number;
|
||||
}
|
||||
|
||||
ParsedInput parsed{source_path, ContentIdentity(bytes), {}};
|
||||
for (std::size_t line_index = 0U; line_index < physical_lines.size();
|
||||
++line_index) {
|
||||
const PhysicalLine& line = physical_lines[line_index];
|
||||
const std::string& trimmed_line = line.trimmed_line;
|
||||
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) {
|
||||
if (trimmed_line.front() == '*') {
|
||||
const auto fields = SplitFields(trimmed_line);
|
||||
const std::string keyword_text =
|
||||
fields.empty() ? std::string{}
|
||||
: Trim(std::string_view{fields[0]}.substr(1U));
|
||||
if (keyword_text.empty()) {
|
||||
return Failure(source_path, line.line_number, "malformed-keyword",
|
||||
trimmed_line,
|
||||
"A keyword line requires a non-empty keyword name.");
|
||||
}
|
||||
|
||||
KeywordBlock block{UppercaseAscii(keyword_text),
|
||||
line.original_line,
|
||||
{line.original_line},
|
||||
{},
|
||||
{},
|
||||
{source_path, line.line_number}};
|
||||
bool continuation = IsKeywordContinuation(trimmed_line);
|
||||
if (!AppendKeywordParameters(block, fields, 1U, continuation)) {
|
||||
return Failure(source_path, line.line_number, "malformed-keyword",
|
||||
block.canonical_name,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
|
||||
while (continuation) {
|
||||
++line_index;
|
||||
if (line_index >= physical_lines.size()) {
|
||||
return Failure(source_path, line.line_number, "malformed-keyword",
|
||||
block.canonical_name,
|
||||
"A keyword continuation requires a following line.");
|
||||
}
|
||||
const PhysicalLine& continuation_line = physical_lines[line_index];
|
||||
if (continuation_line.trimmed_line.empty() ||
|
||||
continuation_line.trimmed_line.rfind("**", 0U) == 0U ||
|
||||
continuation_line.trimmed_line.front() == '*') {
|
||||
return Failure(source_path, continuation_line.line_number,
|
||||
"malformed-keyword", block.canonical_name,
|
||||
"A keyword continuation cannot be interrupted.");
|
||||
}
|
||||
const auto continuation_fields =
|
||||
SplitFields(continuation_line.trimmed_line);
|
||||
continuation =
|
||||
IsKeywordContinuation(continuation_line.trimmed_line);
|
||||
if (!AppendKeywordParameters(block, continuation_fields, 0U,
|
||||
continuation)) {
|
||||
return Failure(source_path, continuation_line.line_number,
|
||||
"malformed-keyword", block.canonical_name,
|
||||
"A keyword parameter name cannot be empty.");
|
||||
}
|
||||
block.physical_lines.push_back(continuation_line.original_line);
|
||||
}
|
||||
parsed.blocks.push_back(std::move(block));
|
||||
} else {
|
||||
if (parsed.blocks.empty()) {
|
||||
return Failure(source_path, line.line_number, "orphan-data-line", "",
|
||||
"A data line must follow a keyword line.");
|
||||
}
|
||||
parsed.blocks.back().data.push_back(
|
||||
{SplitFields(line.original_line), {source_path, line.line_number}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Result<ParsedInput>::Success(std::move(parsed));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#include "io/abaqus/surface_definition_mapper.h"
|
||||
|
||||
#include "io/abaqus/domain_mapping_context.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
Status SurfaceDefinitionMapper::Map(DomainMappingContext& context) const {
|
||||
return context.FinalizeElementSurfaces();
|
||||
}
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
@@ -0,0 +1,20 @@
|
||||
#ifndef FESA_IO_ABAQUS_SURFACE_DEFINITION_MAPPER_H_
|
||||
#define FESA_IO_ABAQUS_SURFACE_DEFINITION_MAPPER_H_
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
class DomainMappingContext;
|
||||
|
||||
/// @brief Resolves assembly-level element surfaces into Domain candidates.
|
||||
class SurfaceDefinitionMapper {
|
||||
public:
|
||||
/// @brief Maps parsed surface members after topology expansion.
|
||||
/// @return Success or the first structured surface mapping failure.
|
||||
Status Map(DomainMappingContext& context) const;
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_SURFACE_DEFINITION_MAPPER_H_
|
||||
@@ -41,6 +41,7 @@ StepDefinition::StepDefinition(StaticStepDefinition definition)
|
||||
concentrated_loads_view_.Add(*owned);
|
||||
loads_.push_back(std::move(owned));
|
||||
}
|
||||
surface_pressure_loads_ = std::move(definition.surface_pressure_loads);
|
||||
}
|
||||
|
||||
const std::string& StepDefinition::Name() const noexcept { return name_; }
|
||||
@@ -62,6 +63,11 @@ StepDefinition::ConcentratedLoads() const noexcept {
|
||||
return concentrated_loads_view_;
|
||||
}
|
||||
|
||||
const std::vector<SurfacePressureLoadDefinition>&
|
||||
StepDefinition::SurfacePressureLoads() const noexcept {
|
||||
return surface_pressure_loads_;
|
||||
}
|
||||
|
||||
StaticProcedureKind StepDefinition::ProcedureKind() const noexcept {
|
||||
return procedure_kind_;
|
||||
}
|
||||
@@ -70,6 +76,8 @@ bool StepDefinition::IsNonlinearStatic() const noexcept {
|
||||
return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
|
||||
}
|
||||
|
||||
bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; }
|
||||
|
||||
double StepDefinition::InitialIncrement() const noexcept {
|
||||
return initial_increment_;
|
||||
}
|
||||
@@ -148,6 +156,11 @@ const std::vector<ElementSet>& Domain::ElementSets() const noexcept {
|
||||
return definition_.element_sets;
|
||||
}
|
||||
|
||||
const std::vector<ElementSurfaceDefinition>& Domain::ElementSurfaces()
|
||||
const noexcept {
|
||||
return definition_.element_surfaces;
|
||||
}
|
||||
|
||||
const DomainCollectionView<StepDefinition>& Domain::Steps() const noexcept {
|
||||
return steps_view_;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user