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