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
+4
View File
@@ -44,6 +44,10 @@ class AnalysisModel {
/// @brief Returns concentrated-load indices in source order. /// @brief Returns concentrated-load indices in source order.
const std::vector<EntityIndex>& ActiveLoads() const noexcept; const std::vector<EntityIndex>& ActiveLoads() const noexcept;
/// @brief Returns active surface pressure records without copying Domain data.
const std::vector<SurfacePressureLoadDefinition>& ActiveSurfacePressureLoads()
const noexcept;
private: private:
/// @brief Builds stable indices without copying the referenced Domain. /// @brief Builds stable indices without copying the referenced Domain.
explicit AnalysisModel(const Domain& domain); explicit AnalysisModel(const Domain& domain);
+1
View File
@@ -28,6 +28,7 @@ struct DataLine {
struct KeywordBlock { struct KeywordBlock {
std::string canonical_name; std::string canonical_name;
std::string original_line; std::string original_line;
std::vector<std::string> physical_lines;
std::vector<KeywordParameter> parameters; std::vector<KeywordParameter> parameters;
std::vector<DataLine> data; std::vector<DataLine> data;
SourceLocation location; SourceLocation location;
+11
View File
@@ -74,12 +74,19 @@ class StepDefinition {
const DomainCollectionView<ConcentratedNodalLoad>& ConcentratedLoads() const DomainCollectionView<ConcentratedNodalLoad>& ConcentratedLoads()
const noexcept; const noexcept;
/// @brief Returns additive surface pressure records in source order.
const std::vector<SurfacePressureLoadDefinition>& SurfacePressureLoads()
const noexcept;
/// @brief Returns the approved static procedure identity. /// @brief Returns the approved static procedure identity.
StaticProcedureKind ProcedureKind() const noexcept; StaticProcedureKind ProcedureKind() const noexcept;
/// @brief Reports whether this step uses the nonlinear static procedure. /// @brief Reports whether this step uses the nonlinear static procedure.
bool IsNonlinearStatic() const noexcept; bool IsNonlinearStatic() const noexcept;
/// @brief Reports the source NLGEOM setting for this static step.
bool UsesNonlinearGeometry() const noexcept;
/// @brief Returns the static initial increment provenance value. /// @brief Returns the static initial increment provenance value.
double InitialIncrement() const noexcept; double InitialIncrement() const noexcept;
@@ -109,6 +116,7 @@ class StepDefinition {
std::vector<std::unique_ptr<Load>> loads_; std::vector<std::unique_ptr<Load>> loads_;
LoadView loads_view_; LoadView loads_view_;
DomainCollectionView<ConcentratedNodalLoad> concentrated_loads_view_; DomainCollectionView<ConcentratedNodalLoad> concentrated_loads_view_;
std::vector<SurfacePressureLoadDefinition> surface_pressure_loads_;
StaticProcedureKind procedure_kind_; StaticProcedureKind procedure_kind_;
bool nlgeom_; bool nlgeom_;
double initial_increment_; double initial_increment_;
@@ -173,6 +181,9 @@ class Domain {
/// @brief Returns element sets in stable declaration order. /// @brief Returns element sets in stable declaration order.
const std::vector<ElementSet>& ElementSets() const noexcept; const std::vector<ElementSet>& ElementSets() const noexcept;
/// @brief Returns assembly element surfaces in stable declaration order.
const std::vector<ElementSurfaceDefinition>& ElementSurfaces() const noexcept;
/// @brief Returns static steps in stable declaration order. /// @brief Returns static steps in stable declaration order.
const DomainCollectionView<StepDefinition>& Steps() const noexcept; const DomainCollectionView<StepDefinition>& Steps() const noexcept;
+76 -4
View File
@@ -53,6 +53,56 @@ struct NodalLoad {
SourceLocation location; SourceLocation location;
}; };
/// @brief Identifies the selected exterior shell surface side.
enum class ShellSurfaceSide { kSpos, kSneg };
/// @brief Identifies how a surface data member resolved its element facets.
enum class SurfaceMemberKind { kDirect, kElset };
/// @brief Stores one resolved S4/S4R facet in an assembly element surface.
struct ElementSurfaceFacetDefinition {
EntityIndex element_index;
std::size_t facet_ordinal;
std::string part_name_raw;
std::string part_name_canonical;
std::string instance_name_raw;
std::string instance_name_canonical;
std::int64_t source_element_label;
std::string source_element_label_raw;
std::string source_element_type;
ShellSurfaceSide side;
SourceLocation first_member_location;
std::size_t first_member_ordinal;
SurfaceMemberKind member_kind;
std::string elset_name_raw;
std::string elset_name_canonical;
};
/// @brief Stores one immutable named assembly-level element surface.
struct ElementSurfaceDefinition {
std::string surface_name_raw;
std::string surface_name_canonical;
std::vector<ElementSurfaceFacetDefinition> facets;
SourceLocation declaration_location;
std::size_t declaration_ordinal;
};
/// @brief Stores one ordered uniform pressure record targeting a surface.
struct SurfacePressureLoadDefinition {
std::string step_name_raw;
std::string step_name_canonical;
EntityIndex surface_index;
std::string surface_name_raw;
std::string surface_name_canonical;
std::string load_type;
double magnitude;
std::string magnitude_raw;
SourceLocation keyword_location;
SourceLocation data_location;
std::size_t data_ordinal;
std::size_t load_ordinal;
};
/// @brief Identifies the approved static procedure semantics for one step. /// @brief Identifies the approved static procedure semantics for one step.
enum class StaticProcedureKind { enum class StaticProcedureKind {
kLinearSmallDisplacement, kLinearSmallDisplacement,
@@ -66,6 +116,7 @@ struct StaticStepDefinition {
std::string step_name, std::string step_name,
std::vector<PrescribedDisplacementDefinition> step_boundaries, std::vector<PrescribedDisplacementDefinition> step_boundaries,
std::vector<NodalLoad> step_loads, std::vector<NodalLoad> step_loads,
std::vector<SurfacePressureLoadDefinition> step_surface_pressure_loads,
StaticProcedureKind step_procedure_kind, StaticProcedureKind step_procedure_kind,
bool step_nlgeom, bool step_nlgeom,
double step_initial_increment, double step_initial_increment,
@@ -73,10 +124,11 @@ struct StaticStepDefinition {
double step_minimum_increment, double step_minimum_increment,
double step_maximum_increment, double step_maximum_increment,
SourceLocation step_location) SourceLocation step_location)
: name{std::move(step_name)}, : name{std::move(step_name)},
boundaries{std::move(step_boundaries)}, boundaries{std::move(step_boundaries)},
loads{std::move(step_loads)}, loads{std::move(step_loads)},
procedure_kind{step_procedure_kind}, surface_pressure_loads{std::move(step_surface_pressure_loads)},
procedure_kind{step_procedure_kind},
nlgeom{step_nlgeom}, nlgeom{step_nlgeom},
initial_increment{step_initial_increment}, initial_increment{step_initial_increment},
time_period{step_time_period}, time_period{step_time_period},
@@ -84,6 +136,24 @@ struct StaticStepDefinition {
maximum_increment{step_maximum_increment}, maximum_increment{step_maximum_increment},
location{std::move(step_location)} {} location{std::move(step_location)} {}
/// @brief Creates a static step without surface pressure records.
StaticStepDefinition(
std::string step_name,
std::vector<PrescribedDisplacementDefinition> step_boundaries,
std::vector<NodalLoad> step_loads,
StaticProcedureKind step_procedure_kind,
bool step_nlgeom,
double step_initial_increment,
double step_time_period,
double step_minimum_increment,
double step_maximum_increment,
SourceLocation step_location)
: StaticStepDefinition(
std::move(step_name), std::move(step_boundaries),
std::move(step_loads), {}, step_procedure_kind, step_nlgeom,
step_initial_increment, step_time_period, step_minimum_increment,
step_maximum_increment, std::move(step_location)) {}
/// @brief Creates a legacy linear static step definition. /// @brief Creates a legacy linear static step definition.
StaticStepDefinition( StaticStepDefinition(
std::string step_name, std::string step_name,
@@ -104,6 +174,7 @@ struct StaticStepDefinition {
std::string name; std::string name;
std::vector<PrescribedDisplacementDefinition> boundaries; std::vector<PrescribedDisplacementDefinition> boundaries;
std::vector<NodalLoad> loads; std::vector<NodalLoad> loads;
std::vector<SurfacePressureLoadDefinition> surface_pressure_loads;
StaticProcedureKind procedure_kind; StaticProcedureKind procedure_kind;
bool nlgeom; bool nlgeom;
double initial_increment; double initial_increment;
@@ -172,6 +243,7 @@ struct ModelDefinition {
std::vector<ElementSet> element_sets; std::vector<ElementSet> element_sets;
std::vector<PartDefinition> parts; std::vector<PartDefinition> parts;
std::vector<InstanceDefinition> instances; std::vector<InstanceDefinition> instances;
std::vector<ElementSurfaceDefinition> element_surfaces;
std::vector<StaticStepDefinition> steps; std::vector<StaticStepDefinition> steps;
std::vector<Diagnostic> warnings; std::vector<Diagnostic> warnings;
}; };
+1
View File
@@ -33,6 +33,7 @@ add_library(
io/abaqus/input_reader.cpp io/abaqus/input_reader.cpp
io/abaqus/material_property_mapper.cpp io/abaqus/material_property_mapper.cpp
io/abaqus/step_definition_mapper.cpp io/abaqus/step_definition_mapper.cpp
io/abaqus/surface_definition_mapper.cpp
io/abaqus/topology_mapper.cpp io/abaqus/topology_mapper.cpp
io/hdf5/hdf5_atomic_file.cpp io/hdf5/hdf5_atomic_file.cpp
io/hdf5/hdf5_model_writer.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_; return active_loads_;
} }
const std::vector<SurfacePressureLoadDefinition>&
AnalysisModel::ActiveSurfacePressureLoads() const noexcept {
return Step().SurfacePressureLoads();
}
AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} { AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
std::vector<bool> reachable_materials(domain.Materials().Size(), false); std::vector<bool> reachable_materials(domain.Materials().Size(), false);
std::vector<bool> reachable_properties(domain.Properties().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/domain_mapping_context.h"
#include "io/abaqus/material_property_mapper.h" #include "io/abaqus/material_property_mapper.h"
#include "io/abaqus/step_definition_mapper.h" #include "io/abaqus/step_definition_mapper.h"
#include "io/abaqus/surface_definition_mapper.h"
#include "io/abaqus/topology_mapper.h" #include "io/abaqus/topology_mapper.h"
namespace fesa { namespace fesa {
@@ -26,6 +27,10 @@ Result<Domain> AbaqusDomainMapper::Map(const ParsedInput& input) const {
if (!status.IsOk()) { if (!status.IsOk()) {
return Result<Domain>::Failure(std::move(status)); 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); status = abaqus_internal::StepDefinitionMapper{}.Map(context);
if (!status.IsOk()) { if (!status.IsOk()) {
return Result<Domain>::Failure(std::move(status)); return Result<Domain>::Failure(std::move(status));
+488 -6
View File
@@ -11,6 +11,7 @@
#include <set> #include <set>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
@@ -29,6 +30,15 @@ std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
return 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 { struct RawNode {
std::int64_t label; std::int64_t label;
std::string label_text; std::string label_text;
@@ -95,6 +105,30 @@ struct RawAssemblySet {
SourceLocation location; 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 { struct RawMaterial {
std::string name; std::string name;
double youngs_modulus{0.0}; double youngs_modulus{0.0};
@@ -106,6 +140,8 @@ struct RawMaterial {
struct RawStep { struct RawStep {
SourceLocation location; SourceLocation location;
std::string name_raw;
std::string name_canonical;
bool has_static{false}; bool has_static{false};
StaticProcedureKind procedure_kind{ StaticProcedureKind procedure_kind{
StaticProcedureKind::kLinearSmallDisplacement}; StaticProcedureKind::kLinearSmallDisplacement};
@@ -113,6 +149,8 @@ struct RawStep {
std::array<double, 4> static_values{}; std::array<double, 4> static_values{};
std::vector<PrescribedDisplacementDefinition> boundaries; std::vector<PrescribedDisplacementDefinition> boundaries;
std::vector<NodalLoad> loads; std::vector<NodalLoad> loads;
std::vector<RawSurfacePressureLoad> surface_pressure_loads;
std::size_t next_load_ordinal{0U};
}; };
struct MappingFailure { struct MappingFailure {
@@ -173,6 +211,13 @@ class DomainMappingContext::Impl {
return CurrentStatus(); return CurrentStatus();
} }
Status FinalizeElementSurfaces() {
if (!failure_) {
ResolveElementSurfaces();
}
return CurrentStatus();
}
Status FinalizeStepCandidate() { Status FinalizeStepCandidate() {
if (!failure_) { if (!failure_) {
FinalizeStep(); FinalizeStep();
@@ -346,6 +391,55 @@ class DomainMappingContext::Impl {
return true; 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, bool ParseNonlinearStaticDouble(const std::string& text, double& value,
const SourceLocation& location, const SourceLocation& location,
const std::string& keyword) { 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() { void ParseBlocks() {
definition_.source_path = input_.source_path; definition_.source_path = input_.source_path;
definition_.source_content_identity = input_.source_content_identity; definition_.source_content_identity = input_.source_content_identity;
@@ -1159,6 +1261,13 @@ class DomainMappingContext::Impl {
} }
assembly_set_seen_ = true; assembly_set_seen_ = true;
ParseAssemblySet(block, false); 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 { } else {
RejectUnknown(block); RejectUnknown(block);
} }
@@ -1217,7 +1326,11 @@ class DomainMappingContext::Impl {
void ParseAssemblySet(const KeywordBlock& block, bool node_set) { void ParseAssemblySet(const KeywordBlock& block, bool node_set) {
const std::string_view name_parameter = node_set ? "NSET" : "ELSET"; 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; return;
} }
const auto* name = RequiredParameterValue(block, name_parameter); const auto* name = RequiredParameterValue(block, name_parameter);
@@ -1232,6 +1345,13 @@ class DomainMappingContext::Impl {
"GENERATE is a valueless flag."); "GENERATE is a valueless flag.");
return; 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)) { if (ContainsAssemblySetName(node_set, *name)) {
InputFailure("duplicate-entity", block.location, block.canonical_name, InputFailure("duplicate-entity", block.location, block.canonical_name,
*name, *name,
@@ -1246,6 +1366,59 @@ class DomainMappingContext::Impl {
assembly_sets_.push_back(std::move(set)); 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) { void ParseMaterial(const KeywordBlock& block) {
if (!assembly_seen_ || model_boundary_seen_) { if (!assembly_seen_ || model_boundary_seen_) {
InvalidKeywordLocation(block, 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) { void ParseStepStart(const KeywordBlock& block) {
if (step_seen_) { if (step_seen_) {
const auto* nlgeom = Parameter(block, "NLGEOM"); const auto* nlgeom = Parameter(block, "NLGEOM");
@@ -1444,10 +1650,21 @@ class DomainMappingContext::Impl {
return; 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; step_seen_ = true;
in_step_ = 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_load_seen_ = false;
step_no_op_seen_ = false; step_no_op_seen_ = false;
} }
@@ -1476,6 +1693,7 @@ class DomainMappingContext::Impl {
if (block.canonical_name == "STATIC") { if (block.canonical_name == "STATIC") {
active_output_ = false; active_output_ = false;
if (!step_.boundaries.empty() || !step_.loads.empty() || if (!step_.boundaries.empty() || !step_.loads.empty() ||
!step_.surface_pressure_loads.empty() ||
step_no_op_seen_) { step_no_op_seen_) {
InvalidKeywordLocation( InvalidKeywordLocation(
block, "STATIC must be the first keyword in the sole step."); block, "STATIC must be the first keyword in the sole step.");
@@ -1501,6 +1719,19 @@ class DomainMappingContext::Impl {
return; return;
} }
ParseCload(block, step_.loads); 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_; step_load_seen_ = !failure_;
} else if (block.canonical_name == "RESTART") { } else if (block.canonical_name == "RESTART") {
active_output_ = false; 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. /// @brief Builds one stable node-target index from the completed candidate.
SourceTargetIndex BuildSourceTargetIndex() const { SourceTargetIndex BuildSourceTargetIndex() const {
std::vector<SourceTargetIndexEntry> entries; std::vector<SourceTargetIndexEntry> entries;
@@ -2257,13 +2703,44 @@ class DomainMappingContext::Impl {
return; 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 // Static time-control values are provenance only: V0 creates exactly
// the canonical final frame 0 and performs no increment loop here. // the canonical final frame 0 and performs no increment loop here.
definition_.steps.push_back( definition_.steps.push_back(
{"Step-1", std::move(boundaries), step_.loads, step_.procedure_kind, {"Step-1", std::move(boundaries), step_.loads,
step_.nlgeom, step_.static_values[0], step_.static_values[1], std::move(surface_pressure_loads), step_.procedure_kind, step_.nlgeom,
step_.static_values[2], step_.static_values[3], step_.location}); step_.static_values[0], step_.static_values[1], step_.static_values[2],
step_.static_values[3], step_.location});
} }
const ParsedInput& input_; const ParsedInput& input_;
@@ -2273,6 +2750,7 @@ class DomainMappingContext::Impl {
std::vector<RawPart> parts_; std::vector<RawPart> parts_;
std::vector<RawInstance> instances_; std::vector<RawInstance> instances_;
std::vector<RawAssemblySet> assembly_sets_; std::vector<RawAssemblySet> assembly_sets_;
std::vector<RawElementSurface> element_surfaces_;
std::vector<RawMaterial> materials_; std::vector<RawMaterial> materials_;
std::vector<PrescribedDisplacementDefinition> model_boundaries_; std::vector<PrescribedDisplacementDefinition> model_boundaries_;
RawStep step_{}; RawStep step_{};
@@ -2322,6 +2800,10 @@ Status DomainMappingContext::FinalizeTopology() {
return impl_->FinalizeTopology(); return impl_->FinalizeTopology();
} }
Status DomainMappingContext::FinalizeElementSurfaces() {
return impl_->FinalizeElementSurfaces();
}
Status DomainMappingContext::FinalizeStep() { Status DomainMappingContext::FinalizeStep() {
return impl_->FinalizeStepCandidate(); return impl_->FinalizeStepCandidate();
} }
@@ -12,6 +12,7 @@ namespace fesa::abaqus_internal {
class DomainBuilder; class DomainBuilder;
class MaterialPropertyMapper; class MaterialPropertyMapper;
class SurfaceDefinitionMapper;
class StepDefinitionMapper; class StepDefinitionMapper;
class TopologyMapper; class TopologyMapper;
@@ -53,6 +54,7 @@ class DomainMappingContext {
friend class DomainBuilder; friend class DomainBuilder;
friend class MaterialPropertyMapper; friend class MaterialPropertyMapper;
friend class SurfaceDefinitionMapper;
friend class StepDefinitionMapper; friend class StepDefinitionMapper;
friend class TopologyMapper; friend class TopologyMapper;
@@ -65,6 +67,9 @@ class DomainMappingContext {
/// @brief Expands identity instances and stable topology candidates. /// @brief Expands identity instances and stable topology candidates.
Status FinalizeTopology(); Status FinalizeTopology();
/// @brief Resolves assembly-level element surfaces after topology expansion.
Status FinalizeElementSurfaces();
/// @brief Resolves the sole static step against finalized source targets. /// @brief Resolves the sole static step against finalized source targets.
Status FinalizeStep(); Status FinalizeStep();
+129 -52
View File
@@ -26,6 +26,10 @@ bool IsAsciiWhitespace(char value) noexcept {
value == '\f' || value == '\v'; value == '\f' || value == '\v';
} }
bool IsAllowedInputCharacter(unsigned char value) noexcept {
return value == '\t' || (value >= 0x20U && value <= 0x7eU);
}
std::string Trim(std::string_view text) { std::string Trim(std::string_view text) {
while (!text.empty() && IsAsciiWhitespace(text.front())) { while (!text.empty() && IsAsciiWhitespace(text.front())) {
text.remove_prefix(1U); text.remove_prefix(1U);
@@ -61,6 +65,44 @@ std::vector<std::string> SplitFields(std::string_view line) {
return fields; 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. /// @brief Computes the stable identity of the exact source bytes.
/// @note This runs before line-ending handling so parser provenance is not /// @note This runs before line-ending handling so parser provenance is not
/// affected by text normalization. /// affected by text normalization.
@@ -112,7 +154,13 @@ Result<ParsedInput> AbaqusInputReader::Read(
"The Abaqus input file could not be read completely."); "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_start = 0U;
std::size_t line_number = 1U; std::size_t line_number = 1U;
while (line_start < bytes.size()) { while (line_start < bytes.size()) {
@@ -123,60 +171,20 @@ Result<ParsedInput> AbaqusInputReader::Read(
if (!original_line.empty() && original_line.back() == '\r') { if (!original_line.empty() && original_line.back() == '\r') {
original_line.pop_back(); original_line.pop_back();
} }
if (original_line.size() > 256U) {
const std::string trimmed_line = Trim(original_line); return Failure(source_path, line_number, "input-line-too-long", "",
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) { "Each physical input line is limited to 256 ASCII bytes.");
if (trimmed_line.front() == '*') { }
const auto fields = SplitFields(trimmed_line); for (const unsigned char character : original_line) {
const std::string keyword_text = if (!IsAllowedInputCharacter(character)) {
fields.empty() ? std::string{} return Failure(source_path, line_number, "invalid-input-character", "",
: Trim(std::string_view{fields[0]}.substr(1U)); "Input lines must use printable 7-bit ASCII or tabs.");
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}});
} }
} }
physical_lines.push_back(
{original_line, Trim(original_line), line_number});
if (newline == std::string::npos) { if (newline == std::string::npos) {
break; break;
} }
@@ -184,6 +192,75 @@ Result<ParsedInput> AbaqusInputReader::Read(
++line_number; ++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)); 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); concentrated_loads_view_.Add(*owned);
loads_.push_back(std::move(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_; } const std::string& StepDefinition::Name() const noexcept { return name_; }
@@ -62,6 +63,11 @@ StepDefinition::ConcentratedLoads() const noexcept {
return concentrated_loads_view_; return concentrated_loads_view_;
} }
const std::vector<SurfacePressureLoadDefinition>&
StepDefinition::SurfacePressureLoads() const noexcept {
return surface_pressure_loads_;
}
StaticProcedureKind StepDefinition::ProcedureKind() const noexcept { StaticProcedureKind StepDefinition::ProcedureKind() const noexcept {
return procedure_kind_; return procedure_kind_;
} }
@@ -70,6 +76,8 @@ bool StepDefinition::IsNonlinearStatic() const noexcept {
return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton; return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
} }
bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; }
double StepDefinition::InitialIncrement() const noexcept { double StepDefinition::InitialIncrement() const noexcept {
return initial_increment_; return initial_increment_;
} }
@@ -148,6 +156,11 @@ const std::vector<ElementSet>& Domain::ElementSets() const noexcept {
return definition_.element_sets; return definition_.element_sets;
} }
const std::vector<ElementSurfaceDefinition>& Domain::ElementSurfaces()
const noexcept {
return definition_.element_surfaces;
}
const DomainCollectionView<StepDefinition>& Domain::Steps() const noexcept { const DomainCollectionView<StepDefinition>& Domain::Steps() const noexcept {
return steps_view_; return steps_view_;
} }
+1
View File
@@ -35,6 +35,7 @@ add_executable(
unit/io/abaqus/domain_mapper_test.cpp unit/io/abaqus/domain_mapper_test.cpp
unit/io/abaqus/input_reader_test.cpp unit/io/abaqus/input_reader_test.cpp
unit/io/abaqus/input_syntax_test.cpp unit/io/abaqus/input_syntax_test.cpp
unit/io/abaqus/shell_surface_pressure_domain_test.cpp
unit/io/hdf5/hdf5_results_writer_test.cpp unit/io/hdf5/hdf5_results_writer_test.cpp
unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp
unit/io/hdf5/hdf5_writer_components_test.cpp unit/io/hdf5/hdf5_writer_components_test.cpp
@@ -121,3 +121,31 @@ TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
EXPECT_EQ(ReadExactBytes(input_path), bytes_before); EXPECT_EQ(ReadExactBytes(input_path), bytes_before);
EXPECT_EQ(std::filesystem::last_write_time(input_path), timestamp_before); EXPECT_EQ(std::filesystem::last_write_time(input_path), timestamp_before);
} }
TEST(InpSyntax, AcceptsAsciiKeywordContinuationAndPreservesPhysicalSourceLines) {
const TemporaryInputFile input{
"keyword-continuation",
"*SuRfAcE, TYPE=ELEMENT,\r\n"
" NAME=Pressure_Surface\r\n"
"Instance-A.0010, SPOS\r\n"};
const auto result = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(result.HasValue());
ASSERT_EQ(result.Value().blocks.size(), 1U);
const auto& block = result.Value().blocks[0];
EXPECT_EQ(block.canonical_name, "SURFACE");
EXPECT_EQ(block.original_line, "*SuRfAcE, TYPE=ELEMENT,");
EXPECT_EQ(block.physical_lines,
(std::vector<std::string>{"*SuRfAcE, TYPE=ELEMENT,",
" NAME=Pressure_Surface"}));
ASSERT_EQ(block.parameters.size(), 2U);
EXPECT_EQ(block.parameters[0].name, "TYPE");
ASSERT_TRUE(block.parameters[0].value.has_value());
EXPECT_EQ(*block.parameters[0].value, "ELEMENT");
EXPECT_EQ(block.parameters[1].name, "NAME");
ASSERT_TRUE(block.parameters[1].value.has_value());
EXPECT_EQ(*block.parameters[1].value, "Pressure_Surface");
ASSERT_EQ(block.data.size(), 1U);
EXPECT_EQ(block.data[0].location.line, 3U);
}
@@ -0,0 +1,304 @@
#include "fesa/analysis/analysis_model.h"
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <type_traits>
#include <utility>
#include <vector>
namespace {
class TemporaryInputFile {
public:
TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-shell-surface-pressure-" + stem + ".inp")} {
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
if (!stream) {
throw std::runtime_error{"Unable to create pressure input fixture."};
}
}
~TemporaryInputFile() {
std::error_code error;
std::filesystem::remove(path_, error);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
fesa::Result<fesa::Domain> MapText(const std::string& stem,
const std::string& content) {
const TemporaryInputFile input{stem, content};
const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
if (!parsed.HasValue()) {
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
}
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
}
std::string ShellDeck(const std::string& assembly_content,
const std::string& dsload_rows) {
return std::string{R"inp(*Part, name=Shell-Part
*Node
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
5, 2., 0., 0.
6, 2., 1., 0.
*Element, type=S4
0010, 1, 2, 3, 4
*Element, type=S4R
0020, 2, 5, 6, 3
*Elset, elset=S4-Set
0010
*Elset, elset=S4R-Set
0020
*Shell Section, elset=S4-Set, material=Steel
0.1
*Shell Section, elset=S4R-Set, material=Steel
0.1
*End Part
*Assembly, name=Assembly
*Instance, name=Instance-B, part=Shell-Part
*End Instance
*Instance, name=Instance-A, part=Shell-Part
*End Instance
)inp"} +
assembly_content + R"inp(*End Assembly
*Material, name=Steel
*Elastic
210000., 0.3
*Step, name=Pressure-Step, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
*Dsload
)inp" +
dsload_rows + "*End Step\n";
}
const fesa::ElementSurfaceDefinition& OnlySurface(const fesa::Domain& domain) {
EXPECT_EQ(domain.ElementSurfaces().size(), 1U);
return domain.ElementSurfaces().front();
}
} // namespace
TEST(ShellSurfacePressureInput,
MapsDirectInstanceFacetsForS4AndS4rCaseInsensitively) {
auto result = MapText(
"direct",
ShellDeck("*SuRfAcE, NAME=Direct-Surface, TYPE=ELEMENT\n"
"instance-a.0010, spos\n"
"INSTANCE-A.0020, sneg\n",
"direct-surface, P, 1.0\n"));
ASSERT_TRUE(result.HasValue());
const auto& surface = OnlySurface(result.Value());
EXPECT_EQ(surface.surface_name_raw, "Direct-Surface");
EXPECT_EQ(surface.surface_name_canonical, "DIRECT-SURFACE");
ASSERT_EQ(surface.facets.size(), 2U);
EXPECT_EQ(surface.facets[0].instance_name_raw, "Instance-A");
EXPECT_EQ(surface.facets[0].source_element_label, 10);
EXPECT_EQ(surface.facets[0].source_element_label_raw, "0010");
EXPECT_EQ(surface.facets[0].source_element_type, "S4");
EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSpos);
EXPECT_EQ(surface.facets[1].source_element_type, "S4R");
EXPECT_EQ(surface.facets[1].side, fesa::ShellSurfaceSide::kSneg);
}
TEST(ShellSurfacePressureInput,
MapsExplicitInstanceElsetAndResultNeutralInternalParameter) {
auto result = MapText(
"elset",
ShellDeck("*ELSET, ELSET=Flat-Members, INTERNAL, INSTANCE=Instance-A\n"
"10, 20\n"
"*SURFACE, TYPE=ELEMENT, NAME=Elset-Surface\n"
"flat-members, SPOS\n",
"Elset-Surface, P, 5.0\n"));
ASSERT_TRUE(result.HasValue());
const auto& surface = OnlySurface(result.Value());
ASSERT_EQ(surface.facets.size(), 2U);
for (const auto& facet : surface.facets) {
EXPECT_EQ(facet.member_kind, fesa::SurfaceMemberKind::kElset);
EXPECT_EQ(facet.elset_name_raw, "flat-members");
EXPECT_EQ(facet.elset_name_canonical, "FLAT-MEMBERS");
EXPECT_EQ(facet.instance_name_raw, "Instance-A");
EXPECT_EQ(facet.side, fesa::ShellSurfaceSide::kSpos);
}
}
TEST(ShellSurfacePressureInput,
MapsMixedDirectAndElsetMembersAcrossTwoExplicitInstancesWithStableDataMemberAndElsetOrder) {
auto result = MapText(
"mixed",
ShellDeck("*ELSET, ELSET=Flat-B, INSTANCE=instance-b\n"
"20\n"
"*ELSET, ELSET=Flat-A, INSTANCE=INSTANCE-A\n"
"10\n"
"*SURFACE, TYPE=ELEMENT, NAME=Mixed-Surface\n"
"instance-a.20, SNEG\n"
"flat-b, SPOS\n"
"INSTANCE-B.10, sneg\n"
"FLAT-A, spos\n",
"Mixed-Surface, P, 3.0\n"));
ASSERT_TRUE(result.HasValue());
const auto& surface = OnlySurface(result.Value());
ASSERT_EQ(surface.facets.size(), 4U);
EXPECT_EQ(surface.facets[0].instance_name_raw, "Instance-B");
EXPECT_EQ(surface.facets[0].source_element_label, 10);
EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSneg);
EXPECT_EQ(surface.facets[0].first_member_ordinal, 2U);
EXPECT_EQ(surface.facets[1].instance_name_raw, "Instance-B");
EXPECT_EQ(surface.facets[1].source_element_label, 20);
EXPECT_EQ(surface.facets[1].member_kind, fesa::SurfaceMemberKind::kElset);
EXPECT_EQ(surface.facets[1].first_member_ordinal, 1U);
EXPECT_EQ(surface.facets[2].instance_name_raw, "Instance-A");
EXPECT_EQ(surface.facets[2].source_element_label, 10);
EXPECT_EQ(surface.facets[2].member_kind, fesa::SurfaceMemberKind::kElset);
EXPECT_EQ(surface.facets[2].first_member_ordinal, 3U);
EXPECT_EQ(surface.facets[3].instance_name_raw, "Instance-A");
EXPECT_EQ(surface.facets[3].source_element_label, 20);
EXPECT_EQ(surface.facets[3].member_kind, fesa::SurfaceMemberKind::kDirect);
EXPECT_EQ(surface.facets[3].first_member_ordinal, 0U);
for (const auto& facet : surface.facets) {
EXPECT_EQ(facet.part_name_raw, "Shell-Part");
EXPECT_EQ(facet.part_name_canonical, "SHELL-PART");
EXPECT_EQ(facet.instance_name_canonical,
facet.instance_name_raw == "Instance-A" ? "INSTANCE-A"
: "INSTANCE-B");
}
}
TEST(ShellSurfacePressureInput,
PreservesSposSnegMagnitudeTextLocationsAndSourceOrder) {
auto result = MapText(
"provenance",
ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n"
"Instance-A.10, SPOS\n"
"Instance-A.20, SNEG\n",
"pressure-surface, p, -0.0\n"
"PRESSURE-SURFACE, P, +2.50e+03\n"));
ASSERT_TRUE(result.HasValue());
const auto& surface = OnlySurface(result.Value());
ASSERT_EQ(surface.facets.size(), 2U);
EXPECT_EQ(surface.facets[0].facet_ordinal, 0U);
EXPECT_EQ(surface.facets[0].side, fesa::ShellSurfaceSide::kSpos);
EXPECT_EQ(surface.facets[1].facet_ordinal, 1U);
EXPECT_EQ(surface.facets[1].side, fesa::ShellSurfaceSide::kSneg);
const auto& step = result.Value().Steps()[0U];
ASSERT_EQ(step.SurfacePressureLoads().size(), 2U);
const auto& first = step.SurfacePressureLoads()[0U];
const auto& second = step.SurfacePressureLoads()[1U];
EXPECT_EQ(first.step_name_raw, "Pressure-Step");
EXPECT_EQ(first.step_name_canonical, "PRESSURE-STEP");
EXPECT_EQ(first.surface_name_raw, "pressure-surface");
EXPECT_EQ(first.surface_name_canonical, "PRESSURE-SURFACE");
EXPECT_EQ(first.load_type, "P");
EXPECT_DOUBLE_EQ(first.magnitude, 0.0);
EXPECT_EQ(first.magnitude_raw, "-0.0");
EXPECT_EQ(first.data_ordinal, 0U);
EXPECT_EQ(first.load_ordinal, 0U);
EXPECT_LT(first.keyword_location.line, first.data_location.line);
EXPECT_EQ(second.magnitude_raw, "+2.50e+03");
EXPECT_DOUBLE_EQ(second.magnitude, 2500.0);
EXPECT_EQ(second.data_ordinal, 1U);
EXPECT_EQ(second.load_ordinal, 1U);
}
TEST(ShellSurfacePressureDomain,
OwnsCanonicalFacetsAndStepOwnedAdditivePressureRecords) {
fesa::ModelDefinition definition{};
definition.element_surfaces = {
{"Surface", "SURFACE", {{0U,
0U,
"Part",
"PART",
"Instance",
"INSTANCE",
10,
"0010",
"S4",
fesa::ShellSurfaceSide::kSpos,
{"owned.inp", 3U},
0U,
fesa::SurfaceMemberKind::kDirect,
"",
""}},
{"owned.inp", 2U},
0U}};
definition.steps = {{"Step", {}, {},
{{"Step", "STEP", 0U, "Surface", "SURFACE", "P",
1.0, "1.0", {"owned.inp", 7U},
{"owned.inp", 8U}, 0U, 0U}},
fesa::StaticProcedureKind::kLinearSmallDisplacement,
false, 0.1, 1.0, 0.01, 1.0, {"owned.inp", 6U}}};
auto result = fesa::Domain::Create(definition);
ASSERT_TRUE(result.HasValue());
definition.element_surfaces[0].facets[0].source_element_label = 99;
definition.steps[0].surface_pressure_loads[0].magnitude = 99.0;
const fesa::Domain& domain = result.Value();
static_assert(std::is_same_v<
decltype(std::declval<const fesa::Domain&>().ElementSurfaces()),
const std::vector<fesa::ElementSurfaceDefinition>&>);
static_assert(std::is_same_v<
decltype(std::declval<const fesa::StepDefinition&>()
.SurfacePressureLoads()),
const std::vector<fesa::SurfacePressureLoadDefinition>&>);
EXPECT_EQ(domain.ElementSurfaces()[0].facets[0].source_element_label, 10);
EXPECT_DOUBLE_EQ(domain.Steps()[0U].SurfacePressureLoads()[0].magnitude,
1.0);
}
TEST(ShellSurfacePressureDomain,
AnalysisModelBorrowsActivePressureWithoutCopyingDomain) {
auto mapped = MapText(
"analysis-model",
ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n"
"Instance-A.10, SPOS\n",
"Pressure-Surface, P, 1.0\n"));
ASSERT_TRUE(mapped.HasValue());
auto model = fesa::AnalysisModel::Create(mapped.Value());
ASSERT_TRUE(model.HasValue());
EXPECT_EQ(&model.Value().ActiveSurfacePressureLoads(),
&mapped.Value().Steps()[0U].SurfacePressureLoads());
EXPECT_EQ(model.Value().ActiveSurfacePressureLoads().size(), 1U);
}
TEST(ShellSurfacePressureInput,
AcceptsWhitespaceSignedZeroAndFiniteScientificNotation) {
auto result = MapText(
"numbers",
ShellDeck("*SURFACE, TYPE=ELEMENT, NAME=Pressure-Surface\n"
"Instance-A.10, SPOS\n",
" pressure-surface , p , +0 \n"
"Pressure-Surface, P, -1.25E-3\n"));
ASSERT_TRUE(result.HasValue());
const auto& loads = result.Value().Steps()[0U].SurfacePressureLoads();
ASSERT_EQ(loads.size(), 2U);
EXPECT_EQ(loads[0].magnitude_raw, "+0");
EXPECT_DOUBLE_EQ(loads[0].magnitude, 0.0);
EXPECT_EQ(loads[1].magnitude_raw, "-1.25E-3");
EXPECT_DOUBLE_EQ(loads[1].magnitude, -0.00125);
}