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
+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();
}