feat(shell-surface-pressure-load): step 1 - map-pressure-input-contract

This commit is contained in:
KOKO\Mimi
2026-09-01 12:34:29 +09:00
parent ccc2f29dbf
commit ca3e4bc1b0
16 changed files with 1102 additions and 62 deletions
+1
View File
@@ -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
+5
View File
@@ -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);
+5
View File
@@ -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));
+488 -6
View File
@@ -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();
+129 -52
View File
@@ -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_
+13
View File
@@ -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_;
}