feat: map fixed-step HHT shell dynamics input

This commit is contained in:
KOKO\Mimi
2026-09-22 21:18:15 +09:00
parent 1dbd8cc5a0
commit 0820259d50
7 changed files with 554 additions and 25 deletions
+13
View File
@@ -88,9 +88,15 @@ class StepDefinition {
/// @brief Reports whether this step defines the frequency procedure.
bool IsFrequency() const noexcept;
/// @brief Reports whether this step defines the HHT dynamic procedure.
bool IsDynamic() const noexcept;
/// @brief Returns frequency source semantics when this is a frequency step.
const FrequencyStepDefinition* Frequency() const noexcept;
/// @brief Returns dynamic source semantics when this is a dynamic step.
const DynamicStepDefinition* Dynamic() const noexcept;
/// @brief Reports the source NLGEOM setting for this static step.
bool UsesNonlinearGeometry() const noexcept;
@@ -118,6 +124,9 @@ class StepDefinition {
/// @brief Converts one parsed frequency-step record to owned semantics.
explicit StepDefinition(FrequencyStepDefinition definition);
/// @brief Converts one parsed dynamic-step record to owned semantics.
explicit StepDefinition(DynamicStepDefinition definition);
std::string name_;
std::vector<std::unique_ptr<BoundaryCondition>> boundary_conditions_;
BoundaryConditionView boundary_conditions_view_;
@@ -135,6 +144,7 @@ class StepDefinition {
double maximum_increment_;
SourceLocation location_;
std::optional<FrequencyStepDefinition> frequency_;
std::optional<DynamicStepDefinition> dynamic_;
};
/// @brief Owns the complete immutable semantic model definition.
@@ -204,6 +214,9 @@ class Domain {
/// @brief Returns the sole frequency source definition when present.
const FrequencyStepDefinition* Frequency() const noexcept;
/// @brief Returns the sole dynamic source definition when present.
const DynamicStepDefinition* Dynamic() const noexcept;
/// @brief Returns sorted nonfatal mapping diagnostics.
const std::vector<Diagnostic>& Warnings() const noexcept;
+19
View File
@@ -208,6 +208,24 @@ struct FrequencyStepDefinition {
std::vector<PrescribedDisplacementDefinition> boundaries;
};
/// @brief Stores the sole supported HHT direct-integration step semantics.
struct DynamicStepDefinition {
std::string source_step_name_raw;
std::string source_step_name_canonical;
SourceLocation step_location;
SourceLocation dynamic_location;
SourceLocation data_location;
double initial_increment{0.0};
double time_period{0.0};
std::uint32_t increment_count{0U};
std::uint32_t maximum_increments{100U};
double alpha{-0.05};
double beta{0.275625};
double gamma{0.55};
std::vector<PrescribedDisplacementDefinition> boundaries;
std::vector<NodalLoad> loads;
};
/// @brief Stores a stable resolved node-set membership list.
struct NodeSet {
std::string name;
@@ -271,6 +289,7 @@ struct ModelDefinition {
std::vector<StaticStepDefinition> steps;
std::vector<MaterialDensityDefinition> densities;
std::optional<FrequencyStepDefinition> frequency_step;
std::optional<DynamicStepDefinition> dynamic_step;
std::vector<Diagnostic> warnings;
};
+254 -22
View File
@@ -147,11 +147,13 @@ struct RawStep {
std::string name_raw;
std::string name_canonical;
bool has_static{false};
std::optional<DynamicStepDefinition> dynamic;
bool perturbation_explicit{false};
bool nlgeom_explicit{false};
StaticProcedureKind procedure_kind{
StaticProcedureKind::kLinearSmallDisplacement};
bool nlgeom{false};
std::uint32_t maximum_increments{0U};
std::array<double, 4> static_values{};
std::optional<FrequencyStepDefinition> frequency;
std::vector<PrescribedDisplacementDefinition> boundaries;
@@ -481,6 +483,25 @@ class DomainMappingContext::Impl {
return true;
}
bool ParseDynamicDouble(const std::string& text, double& value,
const SourceLocation& location,
const std::string& keyword) {
if (text.empty()) {
return InputFailure("invalid-dynamic-data", location, keyword, text,
"Dynamic data fields cannot be empty.");
}
char* end = nullptr;
errno = 0;
const double parsed = std::strtod(text.c_str(), &end);
if (errno == ERANGE || end != text.c_str() + text.size() ||
!std::isfinite(parsed)) {
return InputFailure("invalid-dynamic-data", location, keyword, text,
"Dynamic data fields must be finite.");
}
value = parsed;
return true;
}
bool ParseModelDouble(const std::string& text, double& value,
const SourceLocation& location,
const std::string& keyword,
@@ -1683,7 +1704,12 @@ class DomainMappingContext::Impl {
void ParseBoundary(
const KeywordBlock& block,
std::vector<PrescribedDisplacementDefinition>& destination) {
if (!ValidateParameters(block, {}) || block.data.empty()) {
const bool dynamic_input = std::any_of(
input_.blocks.begin(), input_.blocks.end(),
[](const auto& candidate) { return candidate.canonical_name == "DYNAMIC"; });
if (!ValidateParameters(block, {}, dynamic_input
? "unsupported-dynamic-boundary"
: "invalid-keyword-parameter") || block.data.empty()) {
if (!failure_) {
InputFailure("invalid-data-arity", block.location, block.canonical_name,
"", "BOUNDARY requires one or more data rows.");
@@ -1729,6 +1755,12 @@ class DomainMappingContext::Impl {
}
void ParseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads) {
if (step_.dynamic && !block.parameters.empty()) {
InputFailure("unsupported-dynamic-load", block.location,
block.canonical_name, "",
"Dynamic CLOAD does not support keyword parameters.");
return;
}
if (!ValidateParameters(block, {}) || block.data.empty()) {
if (!failure_) {
InputFailure("invalid-data-arity", block.location, block.canonical_name,
@@ -1755,6 +1787,12 @@ class DomainMappingContext::Impl {
row.fields[0], "CLOAD DOF must be in 1..6.");
return;
}
if (step_.dynamic && dof > 3) {
InputFailure("unsupported-dynamic-load", row.location,
block.canonical_name, row.fields[1],
"Dynamic CLOAD supports global translational DOF 1 through 3 only.");
return;
}
loads.push_back(
{row.fields[0], static_cast<int>(dof), magnitude, row.location});
}
@@ -1803,7 +1841,7 @@ class DomainMappingContext::Impl {
void ParseStepStart(const KeywordBlock& block) {
if (step_seen_) {
if (step_.frequency) {
if (step_.frequency || step_.dynamic) {
InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
@@ -1832,7 +1870,7 @@ class DomainMappingContext::Impl {
"material definitions.");
return;
}
if (!ValidateParameters(block, {"NAME", "NLGEOM", "PERTURBATION"}) ||
if (!ValidateParameters(block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) ||
!RequireNoData(block)) {
return;
}
@@ -1877,6 +1915,29 @@ class DomainMappingContext::Impl {
"STEP NAME must not be empty when it is present.");
return;
}
const auto* increment_limit = Parameter(block, "INC");
std::uint32_t maximum_increments = 100U;
if (increment_limit != nullptr) {
if (!increment_limit->value || increment_limit->value->empty()) {
InputFailure("invalid-dynamic-data", block.location,
block.canonical_name, "INC",
"INC requires a positive uint32 value.");
return;
}
std::int64_t parsed_limit = 0;
if (!ParseInteger(*increment_limit->value, parsed_limit, block.location,
block.canonical_name, true) ||
parsed_limit >
static_cast<std::int64_t>(std::numeric_limits<std::uint32_t>::max())) {
if (!failure_) {
InputFailure("invalid-dynamic-data", block.location,
block.canonical_name, "INC",
"INC requires a positive uint32 value.");
}
return;
}
maximum_increments = static_cast<std::uint32_t>(parsed_limit);
}
step_seen_ = true;
in_step_ = true;
step_ = {};
@@ -1887,6 +1948,7 @@ class DomainMappingContext::Impl {
step_.nlgeom = nlgeom;
step_.nlgeom_explicit = nlgeom_parameter != nullptr;
step_.perturbation_explicit = perturbation != nullptr;
step_.maximum_increments = maximum_increments;
step_load_seen_ = false;
step_no_op_seen_ = false;
}
@@ -1897,7 +1959,7 @@ class DomainMappingContext::Impl {
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
return;
}
if (!step_.has_static && !step_.frequency) {
if (!step_.has_static && !step_.frequency && !step_.dynamic) {
InvalidKeywordLocation(
block, "The sole step requires one supported procedure keyword.");
return;
@@ -1930,9 +1992,19 @@ class DomainMappingContext::Impl {
return;
}
ParseFrequency(block);
} else if (block.canonical_name == "DYNAMIC") {
active_output_ = false;
if (step_.has_static || step_.frequency || step_.dynamic ||
!step_.boundaries.empty() || !step_.loads.empty() ||
!step_.surface_pressure_loads.empty() || step_no_op_seen_) {
InvalidKeywordLocation(
block, "DYNAMIC must be the first procedure in the sole step.");
return;
}
ParseDynamic(block);
} else if (block.canonical_name == "BOUNDARY") {
active_output_ = false;
if ((!step_.has_static && !step_.frequency) || step_load_seen_ ||
if ((!step_.has_static && !step_.frequency && !step_.dynamic) || step_load_seen_ ||
step_no_op_seen_) {
InvalidKeywordLocation(
block,
@@ -1940,13 +2012,22 @@ class DomainMappingContext::Impl {
"and no-op requests.");
return;
}
if (step_.dynamic && !block.parameters.empty()) {
InputFailure("unsupported-dynamic-boundary", block.location,
block.canonical_name, "",
"Dynamic BOUNDARY does not support keyword parameters.");
return;
}
ParseBoundary(block, step_.boundaries);
if (!failure_ && step_.frequency) {
ValidateEigenBoundaries(step_.boundaries);
}
if (!failure_ && step_.dynamic) {
ValidateDynamicBoundaries(step_.boundaries);
}
} else if (block.canonical_name == "CLOAD") {
active_output_ = false;
if (!step_.has_static || step_no_op_seen_) {
if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) {
InvalidKeywordLocation(
block,
"CLOAD follows STATIC and boundaries and precedes no-op requests.");
@@ -1959,7 +2040,7 @@ class DomainMappingContext::Impl {
step_load_seen_ = !failure_;
} else if (block.canonical_name == "DSLOAD") {
active_output_ = false;
if (!step_.has_static || step_no_op_seen_) {
if (!step_.has_static || step_.dynamic || step_no_op_seen_) {
InvalidKeywordLocation(block,
"DSLOAD follows STATIC and boundaries and "
"precedes no-op requests.");
@@ -1969,7 +2050,7 @@ class DomainMappingContext::Impl {
step_load_seen_ = !failure_;
} else if (block.canonical_name == "RESTART") {
active_output_ = false;
if (!step_.has_static && !step_.frequency) {
if (!step_.has_static && !step_.frequency && !step_.dynamic) {
InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
@@ -1984,7 +2065,7 @@ class DomainMappingContext::Impl {
if (!failure_) {
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"Frequency RESTART accepts only WRITE, FREQUENCY=0.");
"Dynamic and frequency RESTART accept only WRITE, FREQUENCY=0.");
}
return;
}
@@ -1993,7 +2074,7 @@ class DomainMappingContext::Impl {
AddIgnoredWarning(block);
}
} else if (block.canonical_name == "OUTPUT") {
if (!step_.has_static && !step_.frequency) {
if (!step_.has_static && !step_.frequency && !step_.dynamic) {
InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
@@ -2160,6 +2241,86 @@ class DomainMappingContext::Impl {
}
}
void ValidateDynamicBoundaries(
const std::vector<PrescribedDisplacementDefinition>& boundaries) {
for (const auto& boundary : boundaries) {
if (boundary.value != 0.0) {
InputFailure("unsupported-dynamic-boundary", boundary.location,
"BOUNDARY", boundary.target,
"Dynamic boundaries must be homogeneous and constant.");
return;
}
}
}
void ParseDynamic(const KeywordBlock& block) {
if (step_.perturbation_explicit || !step_.nlgeom ||
model_element_family_ != ElementFamily::kShell ||
!ValidateParameters(block, {"DIRECT"}, "unsupported-dynamic-parameter")) {
if (!failure_) {
InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"DYNAMIC requires the approved nonlinear shell configuration.");
}
return;
}
const auto* direct = Parameter(block, "DIRECT");
if (direct == nullptr || direct->value || block.data.size() != 1U ||
block.data[0].fields.size() < 2U ||
std::any_of(block.data[0].fields.begin() + 2,
block.data[0].fields.end(),
[](const auto& field) { return !field.empty(); })) {
InputFailure("invalid-dynamic-data", block.location, block.canonical_name,
"", "DYNAMIC,DIRECT requires one row with increment and time period.");
return;
}
DynamicStepDefinition dynamic{step_.name_raw,
step_.name_canonical,
step_.location,
block.location,
block.data[0].location,
0.0,
0.0,
0U,
step_.maximum_increments,
-0.05,
0.275625,
0.55,
{},
{}};
if (!ParseDynamicDouble(block.data[0].fields[0], dynamic.initial_increment,
block.data[0].location, block.canonical_name) ||
!ParseDynamicDouble(block.data[0].fields[1], dynamic.time_period,
block.data[0].location, block.canonical_name) ||
!(dynamic.initial_increment > 0.0) || !(dynamic.time_period > 0.0)) {
if (!failure_) {
InputFailure("invalid-dynamic-data", block.data[0].location,
block.canonical_name, "",
"Dynamic increment and time period must be finite and positive.");
}
return;
}
const double count = dynamic.time_period / dynamic.initial_increment;
const double rounded_count = std::round(count);
if (!std::isfinite(count) || rounded_count < 1.0 ||
rounded_count >
static_cast<double>(std::numeric_limits<std::uint32_t>::max()) ||
dynamic.initial_increment * rounded_count != dynamic.time_period) {
InputFailure("invalid-dynamic-schedule", block.data[0].location,
block.canonical_name, "",
"DYNAMIC,DIRECT requires an exact positive integer number of increments.");
return;
}
dynamic.increment_count = static_cast<std::uint32_t>(rounded_count);
if (dynamic.increment_count > dynamic.maximum_increments) {
InputFailure("invalid-dynamic-schedule", block.data[0].location,
block.canonical_name, "",
"DYNAMIC,DIRECT increment count cannot exceed STEP INC.");
return;
}
step_.dynamic = std::move(dynamic);
}
void ParseStatic(const KeywordBlock& block) {
if (step_.perturbation_explicit || step_.frequency) {
InputFailure("unsupported-analysis-procedure", block.location,
@@ -2214,7 +2375,10 @@ class DomainMappingContext::Impl {
}
void ParseOutputRoot(const KeywordBlock& block) {
if (!ValidateParameters(block, {"FIELD", "HISTORY", "VARIABLE"})) {
if (!ValidateParameters(
block, step_.dynamic
? std::vector<std::string_view>{"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}
: std::vector<std::string_view>{"FIELD", "HISTORY", "VARIABLE"})) {
return;
}
const auto* field = Parameter(block, "FIELD");
@@ -2237,7 +2401,22 @@ class DomainMappingContext::Impl {
"Nonlinear OUTPUT requires VARIABLE=PRESELECT.");
return;
}
if (step_.frequency &&
if (step_.dynamic) {
const auto* frequency = Parameter(block, "FREQUENCY");
if (!ValidateParameters(block, {"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) ||
variable == nullptr || !variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") ||
(frequency != nullptr &&
(!frequency->value || *frequency->value != "1")) ||
!RequireNoData(block)) {
if (!failure_) {
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"Dynamic OUTPUT accepts only VARIABLE=PRESELECT, FREQUENCY=1.");
}
return;
}
} else if (step_.frequency &&
((variable != nullptr &&
(!variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
@@ -2288,7 +2467,9 @@ class DomainMappingContext::Impl {
void AddIgnoredWarning(const KeywordBlock& block) {
// One warning per allowlisted keyword keeps no-op provenance stable;
// subordinate variable rows remain attached to that keyword record.
definition_.warnings.push_back({Severity::kWarning, "ignored-input-keyword",
definition_.warnings.push_back({Severity::kWarning,
step_.dynamic ? "ignored-output-request"
: "ignored-input-keyword",
block.location, block.canonical_name, "",
"The allowlisted Abaqus keyword is "
"ignored without semantic effect."});
@@ -2359,7 +2540,7 @@ class DomainMappingContext::Impl {
void ValidateCardinality() {
if (parts_.empty() || !assembly_seen_ || instances_.empty() ||
materials_.empty() || !step_seen_ ||
(!step_.has_static && !step_.frequency)) {
(!step_.has_static && !step_.frequency && !step_.dynamic)) {
const auto location = input_.blocks.empty()
? SourceLocation{input_.source_path, 0U}
: input_.blocks.back().location;
@@ -2410,7 +2591,7 @@ class DomainMappingContext::Impl {
}
void FinalizeDensities() {
if (!step_.frequency) {
if (!step_.frequency && !step_.dynamic) {
if (std::any_of(materials_.begin(), materials_.end(),
[](const RawMaterial& material) {
return material.density.has_value();
@@ -2420,20 +2601,33 @@ class DomainMappingContext::Impl {
}
return;
}
ValidateFrequencyPreprint();
if (failure_) {
return;
if (step_.frequency) {
ValidateFrequencyPreprint();
if (failure_) {
return;
}
}
if (step_.dynamic) {
ValidateDynamicPreprint();
if (failure_) {
return;
}
for (auto& warning : definition_.warnings) {
if (warning.keyword == "PREPRINT") {
warning.code = "ignored-output-request";
}
}
}
if (model_element_family_ != ElementFamily::kShell) {
InputFailure("unsupported-analysis-procedure", step_.location,
"FREQUENCY", step_.name_raw,
"FREQUENCY requires the approved shell model family.");
step_.frequency ? "FREQUENCY" : "DYNAMIC", step_.name_raw,
"The procedure requires the approved shell model family.");
return;
}
if (!element_surfaces_.empty()) {
InputFailure("unsupported-keyword", element_surfaces_.front().location,
"SURFACE", element_surfaces_.front().name,
"Frequency models do not support element surfaces.");
"The selected procedure does not support element surfaces.");
return;
}
for (std::size_t index = 0U; index < materials_.size(); ++index) {
@@ -2449,7 +2643,7 @@ class DomainMappingContext::Impl {
if (active) {
ModelFailure("missing-material-density", material.location,
"MATERIAL", material.name,
"Each active frequency material requires DENSITY.");
"Each active dynamic or frequency material requires DENSITY.");
return;
}
continue;
@@ -2492,6 +2686,32 @@ class DomainMappingContext::Impl {
}
}
void ValidateDynamicPreprint() {
for (const auto& block : input_.blocks) {
if (block.canonical_name != "PREPRINT") {
continue;
}
if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) ||
block.parameters.size() != 4U) {
if (!failure_) {
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"DYNAMIC requires PREPRINT with ECHO, MODEL, HISTORY, and CONTACT parameters.");
}
return;
}
for (const auto& parameter : block.parameters) {
if (!parameter.value ||
!AsciiCaseInsensitiveEquals(*parameter.value, "NO")) {
InputFailure("unsupported-keyword", block.location,
block.canonical_name, parameter.name,
"DYNAMIC requires PREPRINT parameters to equal NO.");
return;
}
}
}
}
void FinalizePartsAndSections() {
part_section_assignments_.resize(parts_.size());
part_shell_section_assignments_.resize(parts_.size());
@@ -3213,6 +3433,12 @@ class DomainMappingContext::Impl {
return;
}
}
if (step_.dynamic) {
ValidateDynamicBoundaries(boundaries);
if (failure_) {
return;
}
}
const SourceTargetIndex target_index = BuildSourceTargetIndex();
const SourceTargetResolver target_resolver{target_index};
@@ -3250,6 +3476,12 @@ class DomainMappingContext::Impl {
definition_.frequency_step = std::move(step_.frequency);
return;
}
if (step_.dynamic) {
step_.dynamic->boundaries = std::move(boundaries);
step_.dynamic->loads = step_.loads;
definition_.dynamic_step = std::move(step_.dynamic);
return;
}
std::vector<SurfacePressureLoadDefinition> surface_pressure_loads;
surface_pressure_loads.reserve(step_.surface_pressure_loads.size());
for (const auto& load : step_.surface_pressure_loads) {
+57 -2
View File
@@ -69,6 +69,43 @@ StepDefinition::StepDefinition(FrequencyStepDefinition definition)
}
}
StepDefinition::StepDefinition(DynamicStepDefinition definition)
: name_{definition.source_step_name_raw},
procedure_kind_{StaticProcedureKind::kNonlinearFullNewton},
nlgeom_{true},
initial_increment_{definition.initial_increment},
time_period_{definition.time_period},
minimum_increment_{definition.initial_increment},
maximum_increment_{definition.initial_increment},
location_{definition.step_location},
dynamic_{std::move(definition)} {
for (const auto& boundary : dynamic_->boundaries) {
for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof;
++source_dof) {
const std::size_t source_order = boundary_conditions_.size();
auto owned = std::make_unique<PrescribedDisplacementBoundaryCondition>(
SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target},
static_cast<DofComponent>(source_dof - 1), boundary.value,
source_order, boundary.location);
boundary_conditions_view_.push_back(std::cref(*owned));
prescribed_displacements_view_.Add(*owned);
boundary_conditions_.push_back(std::move(owned));
}
}
loads_.reserve(dynamic_->loads.size());
loads_view_.reserve(dynamic_->loads.size());
for (std::size_t source_order = 0U; source_order < dynamic_->loads.size();
++source_order) {
auto& load = dynamic_->loads[source_order];
auto owned = std::make_unique<ConcentratedNodalLoad>(
SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof,
load.magnitude, source_order, std::move(load.location));
loads_view_.push_back(std::cref(*owned));
concentrated_loads_view_.Add(*owned);
loads_.push_back(std::move(owned));
}
}
const std::string& StepDefinition::Name() const noexcept { return name_; }
const BoundaryConditionView& StepDefinition::BoundaryConditions()
@@ -98,17 +135,24 @@ StaticProcedureKind StepDefinition::ProcedureKind() const noexcept {
}
bool StepDefinition::IsNonlinearStatic() const noexcept {
return procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
return !dynamic_ &&
procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton;
}
bool StepDefinition::IsFrequency() const noexcept {
return frequency_.has_value();
}
bool StepDefinition::IsDynamic() const noexcept { return dynamic_.has_value(); }
const FrequencyStepDefinition* StepDefinition::Frequency() const noexcept {
return frequency_ ? &*frequency_ : nullptr;
}
const DynamicStepDefinition* StepDefinition::Dynamic() const noexcept {
return dynamic_ ? &*dynamic_ : nullptr;
}
bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; }
double StepDefinition::InitialIncrement() const noexcept {
@@ -207,6 +251,10 @@ const FrequencyStepDefinition* Domain::Frequency() const noexcept {
return steps_view_.Size() == 1U ? steps_view_[0U].Frequency() : nullptr;
}
const DynamicStepDefinition* Domain::Dynamic() const noexcept {
return steps_view_.Size() == 1U ? steps_view_[0U].Dynamic() : nullptr;
}
const std::vector<Diagnostic>& Domain::Warnings() const noexcept {
return definition_.warnings;
}
@@ -270,7 +318,8 @@ Domain::Domain(ModelDefinition definition)
definition_.shell_elements.clear();
step_definitions_.reserve(definition_.steps.size() +
(definition_.frequency_step ? 1U : 0U));
(definition_.frequency_step ? 1U : 0U) +
(definition_.dynamic_step ? 1U : 0U));
for (auto& step : definition_.steps) {
auto owned =
std::unique_ptr<StepDefinition>(new StepDefinition(std::move(step)));
@@ -284,6 +333,12 @@ Domain::Domain(ModelDefinition definition)
steps_view_.Add(*owned);
step_definitions_.push_back(std::move(owned));
}
if (definition_.dynamic_step) {
auto owned = std::unique_ptr<StepDefinition>(
new StepDefinition(std::move(*definition_.dynamic_step)));
steps_view_.Add(*owned);
step_definitions_.push_back(std::move(owned));
}
}
} // namespace fesa
+1
View File
@@ -38,6 +38,7 @@ add_executable(
unit/io/abaqus/domain_mapping_components_test.cpp
unit/io/abaqus/domain_mapper_test.cpp
unit/io/abaqus/eigen_domain_test.cpp
unit/io/abaqus/hht_dynamic_domain_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
+4 -1
View File
@@ -660,7 +660,10 @@ TEST(InpDomainMapping, RejectsUnsupportedNonlinearProceduresAndMixedModels) {
test_case.second);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kInput);
ASSERT_NE(FindDiagnostic(result.GetStatus(), "unsupported-analysis-procedure"),
ASSERT_NE(FindDiagnostic(result.GetStatus(),
test_case.first == "dynamic"
? "invalid-dynamic-data"
: "unsupported-analysis-procedure"),
nullptr);
}
@@ -0,0 +1,206 @@
#include <gtest/gtest.h>
#include <filesystem>
#include <fstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
namespace {
class TemporaryInputFile {
public:
TemporaryInputFile(const std::string& stem, const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-hht-domain-" + 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 dynamic 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};
auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
if (!parsed.HasValue()) {
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
}
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
}
const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status,
const std::string& code) {
for (const auto& diagnostic : status.Diagnostics()) {
if (diagnostic.code == code) {
return &diagnostic;
}
}
return nullptr;
}
std::string DynamicDeck() {
return R"inp(*Preprint, echo=NO, model=NO, history=NO, contact=NO
*Part, name=ShellPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
3, 1., 1., 0.
4, 0., 1., 0.
*Element, type=S4R
1, 1, 2, 3, 4
*Elset, elset=ShellSet
1
*Shell Section, elset=ShellSet, material=Steel
0.5, 5
*End Part
*Assembly, name=Assembly
*Instance, name=Shell-1, part=ShellPart
*End Instance
*Nset, nset=Root, instance=Shell-1
1
*Nset, nset=Tip, instance=Shell-1
3
*End Assembly
*Material, name=Steel
*Density
2700.,
*Elastic
2.1e11, 0.3
*Boundary
Root, 1, 6
*Step, name=Dynamic-Load, nlgeom=YES, inc=1000
*Dynamic, direct
0.1, 10.,
*Cload
Tip, 1, -1000.
*Restart, write, frequency=0
*Output, field, variable=PRESELECT, frequency=1
*Output, history, variable=PRESELECT
*End Step
)inp";
}
std::string ReplaceOnce(std::string text, const std::string& from,
const std::string& to) {
const auto position = text.find(from);
if (position == std::string::npos) {
throw std::logic_error{"Dynamic fixture mutation source was not found."};
}
text.replace(position, from.size(), to);
return text;
}
std::filesystem::path RepositoryRoot() {
auto path = std::filesystem::path{__FILE__}.parent_path();
for (int parent = 0; parent < 4; ++parent) {
path = path.parent_path();
}
return path;
}
TEST(HhtDynamicDomainMapping, MapsDirectScheduleDensityAndSourceOrder) {
auto result = MapText("accepted", DynamicDeck());
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
const fesa::Domain& domain = result.Value();
ASSERT_NE(domain.Dynamic(), nullptr);
const auto& dynamic = *domain.Dynamic();
EXPECT_EQ(dynamic.source_step_name_raw, "Dynamic-Load");
EXPECT_EQ(dynamic.source_step_name_canonical, "DYNAMIC-LOAD");
EXPECT_DOUBLE_EQ(dynamic.initial_increment, 0.1);
EXPECT_DOUBLE_EQ(dynamic.time_period, 10.0);
EXPECT_EQ(dynamic.increment_count, 100U);
EXPECT_EQ(dynamic.maximum_increments, 1000U);
EXPECT_DOUBLE_EQ(dynamic.alpha, -0.05);
EXPECT_DOUBLE_EQ(dynamic.beta, 0.275625);
EXPECT_DOUBLE_EQ(dynamic.gamma, 0.55);
ASSERT_EQ(dynamic.boundaries.size(), 1U);
ASSERT_EQ(dynamic.loads.size(), 1U);
EXPECT_EQ(dynamic.loads[0U].dof, 1);
ASSERT_EQ(domain.Densities().size(), 1U);
EXPECT_DOUBLE_EQ(domain.Densities()[0U].rho, 2700.0);
ASSERT_EQ(domain.Steps().Size(), 1U);
EXPECT_TRUE(domain.Steps()[0U].IsDynamic());
EXPECT_FALSE(domain.Steps()[0U].IsNonlinearStatic());
EXPECT_EQ(domain.Steps()[0U].Dynamic(), domain.Dynamic());
ASSERT_EQ(domain.Warnings().size(), 4U);
for (const auto& warning : domain.Warnings()) {
EXPECT_EQ(warning.code, "ignored-output-request");
}
}
TEST(HhtDynamicDomainMapping, MapsOriginalReferenceDeckWithoutMutation) {
const auto input_path = RepositoryRoot() / "reference" /
"shellNL-dyn-meshfine" / "shellNL-dyn-meshfine.inp";
auto parsed = fesa::AbaqusInputReader{}.Read(input_path);
ASSERT_TRUE(parsed.HasValue());
auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value());
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
ASSERT_NE(result.Value().Dynamic(), nullptr);
EXPECT_EQ(result.Value().Dynamic()->increment_count, 100U);
EXPECT_EQ(result.Value().Dynamic()->maximum_increments, 1000U);
EXPECT_EQ(result.Value().Dynamic()->loads.size(), 3U);
}
TEST(HhtDynamicDomainMapping, RejectsMalformedScheduleAndUnsupportedControls) {
struct InvalidCase {
std::string name;
std::string deck;
std::string diagnostic;
};
const std::string base = DynamicDeck();
const std::vector<InvalidCase> cases{
{"noninteger-grid", ReplaceOnce(base, "0.1, 10.,", "0.3, 10.,"),
"invalid-dynamic-schedule"},
{"increment-limit", ReplaceOnce(base, "inc=1000", "inc=99"),
"invalid-dynamic-schedule"},
{"nonpositive-data", ReplaceOnce(base, "0.1, 10.,", "0., 10.,"),
"invalid-dynamic-data"},
{"integrator-override",
ReplaceOnce(base, "*Dynamic, direct", "*Dynamic, direct, alpha=-0.1"),
"unsupported-dynamic-parameter"},
{"rotational-load", ReplaceOnce(base, "Tip, 1, -1000.", "Tip, 4, 0."),
"unsupported-dynamic-load"},
{"nonzero-boundary", ReplaceOnce(base, "Root, 1, 6", "Root, 1, 6, 1."),
"unsupported-dynamic-boundary"},
{"amplitude-boundary",
ReplaceOnce(base, "*Boundary\n", "*Boundary, amplitude=A\n"),
"unsupported-dynamic-boundary"},
{"missing-density", ReplaceOnce(base, "*Density\n2700.,\n", ""),
"missing-material-density"},
{"unresolved-load",
ReplaceOnce(base, "Tip, 1, -1000.", "Missing, 1, -1000."),
"unresolved-reference"}};
for (const auto& test_case : cases) {
SCOPED_TRACE(test_case.name);
auto result = MapText(test_case.name, test_case.deck);
ASSERT_FALSE(result.HasValue());
ASSERT_NE(FindDiagnostic(result.GetStatus(), test_case.diagnostic),
nullptr);
}
}
} // namespace