Files
FESADev/src/fesa/io/abaqus/step_definition_mapper.cpp
T

1682 lines
70 KiB
C++

#include "io/abaqus/step_definition_mapper.h"
#include <algorithm>
#include <cerrno>
#include <charconv>
#include <cmath>
#include <cstdlib>
#include <limits>
#include <map>
#include <system_error>
#include <utility>
#include "fesa/analysis/thermal_time_grid.h"
#include "fesa/core/ascii.h"
#include "io/abaqus/domain_mapping_context.h"
#include "io/abaqus/step_definition_candidates.h"
#include "io/abaqus/surface_definition_candidates.h"
namespace fesa::abaqus_internal {
bool StepDefinitionCandidates::HasSupportedProcedure() const noexcept {
return step_.has_static || step_.frequency || step_.dynamic ||
step_.has_heat_transfer;
}
void StepDefinitionCandidates::FillSummary(
DomainMappingCandidateSummary& summary) const noexcept {
summary.model_boundary_count = model_boundaries_.size();
summary.step_boundary_count = step_.boundaries.size();
summary.step_load_count = step_.loads.size();
summary.has_static_step = step_.has_static;
}
MaterialProcedureView StepDefinitionCandidates::MaterialProcedure(
std::optional<MaterialProcedureView::Surface> first_surface,
const std::optional<SourceLocation>& physical_constants) const {
return {step_.frequency.has_value(),
step_.dynamic.has_value(),
step_.location,
step_.name_raw,
std::move(first_surface),
physical_constants};
}
void StepDefinitionCandidates::ParseModelBoundary(const KeywordBlock& block) {
if (model_element_family_ == ElementFamily::kThermalShell) {
ParseThermalBoundary(block, thermal_model_boundaries_);
} else {
ParseBoundary(block, model_boundaries_);
}
}
bool StepDefinitionCandidates::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 diagnostics_.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 diagnostics_.InputFailure(
"invalid-numeric-value", location, keyword, text,
"Pressure exponents require one or more digits.");
}
}
if (cursor != text.size()) {
return diagnostics_.InputFailure(
"invalid-numeric-value", location, keyword, text,
"Pressure values require C-locale decimal syntax.");
}
const char* const begin = text.data();
const char* const end = begin + text.size();
const char* const numeric_begin = *begin == '+' ? begin + 1U : begin;
const auto parsed =
std::from_chars(numeric_begin, end, value, std::chars_format::general);
if (parsed.ec != std::errc{} || parsed.ptr != end || !std::isfinite(value)) {
return diagnostics_.InputFailure(
"invalid-numeric-value", location, keyword, text,
"Pressure values must be finite C-locale values.");
}
return true;
}
bool StepDefinitionCandidates::ParseNonlinearStaticDouble(
const std::string& text, double& value, const SourceLocation& location,
const std::string& keyword) {
if (text.empty()) {
return diagnostics_.InputFailure(
"invalid-nonlinear-static-data", location, keyword, text,
"A nonlinear STATIC field 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 diagnostics_.InputFailure(
"invalid-nonlinear-static-data", location, keyword, text,
"All four nonlinear STATIC fields must be finite.");
}
value = parsed;
return true;
}
bool StepDefinitionCandidates::ParseDynamicDouble(
const std::string& text, double& value, const SourceLocation& location,
const std::string& keyword) {
if (text.empty()) {
return diagnostics_.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 diagnostics_.InputFailure("invalid-dynamic-data", location, keyword,
text,
"Dynamic data fields must be finite.");
}
value = parsed;
return true;
}
void StepDefinitionCandidates::ParseAmplitude(const KeywordBlock& block) {
const bool dynamic_source = std::any_of(
input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) {
return candidate.canonical_name == "DYNAMIC";
});
if (model_element_family_ != ElementFamily::kSolid || !dynamic_source) {
diagnostics_.InputFailure(
"unsupported-load-amplitude", block.location, block.canonical_name, "",
"Amplitude is supported only in Solid dynamic models.");
return;
}
if (!diagnostics_.ValidateParameters(block,
{"NAME", "DEFINITION", "TIME", "VALUE"}))
return;
const auto* name = diagnostics_.RequiredParameterValue(block, "NAME");
if (!name) return;
for (const auto& option : std::array<std::pair<const char*, const char*>, 3>{
{{"DEFINITION", "TABULAR"},
{"TIME", "STEP TIME"},
{"VALUE", "RELATIVE"}}}) {
const auto* parameter = diagnostics_.Parameter(block, option.first);
if (parameter &&
(!parameter->value ||
!AsciiCaseInsensitiveEquals(*parameter->value, option.second))) {
diagnostics_.InputFailure(
"unsupported-amplitude-option", block.location, block.canonical_name,
option.first,
"Only TABULAR STEP TIME RELATIVE amplitude is supported.");
return;
}
}
const std::string canonical = AsciiUppercase(*name);
if (std::any_of(definition_.amplitudes.begin(), definition_.amplitudes.end(),
[&canonical](const auto& a) {
return a.name_canonical == canonical;
})) {
diagnostics_.InputFailure(
"duplicate-amplitude", block.location, block.canonical_name, *name,
"Amplitude names must be unique ignoring ASCII case.");
return;
}
TabularAmplitudeDefinition amplitude{*name, canonical, block.location, {}};
for (const auto& row : block.data) {
if (row.fields.empty() || row.fields.size() % 2U != 0U) {
diagnostics_.InputFailure(
"invalid-amplitude-data", row.location, block.canonical_name, *name,
"Amplitude rows require complete time/value pairs.");
return;
}
for (std::size_t field = 0U; field < row.fields.size(); field += 2U) {
double time = 0.0;
double value = 0.0;
if (!diagnostics_.ParseDouble(row.fields[field], time, row.location,
block.canonical_name) ||
!diagnostics_.ParseDouble(row.fields[field + 1U], value, row.location,
block.canonical_name))
return;
if (time < 0.0 ||
(!amplitude.points.empty() && !(time > amplitude.points.back()[0]))) {
diagnostics_.InputFailure(
"invalid-amplitude-data", row.location, block.canonical_name, *name,
"Amplitude times must be nonnegative and strictly increasing.");
return;
}
amplitude.points.push_back({time, value});
}
}
if (amplitude.points.empty()) {
diagnostics_.InputFailure(
"invalid-amplitude-data", block.location, block.canonical_name, *name,
"Amplitude requires at least one finite time/value pair.");
return;
}
definition_.amplitudes.push_back(std::move(amplitude));
}
void StepDefinitionCandidates::ParseInitialConditions(
const KeywordBlock& block) {
if (model_element_family_ != ElementFamily::kThermalShell ||
!diagnostics_.ValidateParameters(block, {"TYPE"}) || block.data.empty()) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"invalid-data-arity", block.location, block.canonical_name, "",
"Thermal initial conditions require data rows.");
}
return;
}
const auto* type = diagnostics_.RequiredParameterValue(block, "TYPE");
if (type == nullptr || !AsciiCaseInsensitiveEquals(*type, "TEMPERATURE")) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure("unsupported-keyword", block.location,
block.canonical_name, "TYPE",
"Only TYPE=TEMPERATURE is supported.");
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 2U || row.fields[0].empty()) {
diagnostics_.InputFailure("invalid-data-arity", row.location,
block.canonical_name, "",
"Initial temperatures require target, value.");
return;
}
double value = 0.0;
if (!diagnostics_.ParseThermalDouble(row.fields[1], value, row.location,
block.canonical_name))
return;
thermal_initial_conditions_.push_back({row.fields[0], value, row.location});
}
}
void StepDefinitionCandidates::ParseThermalBoundary(
const KeywordBlock& block, std::vector<RawThermalBoundary>& destination) {
if (!diagnostics_.ValidateParameters(block, {}) || block.data.empty()) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure("invalid-data-arity", block.location,
block.canonical_name, "",
"Thermal BOUNDARY requires data rows.");
return;
}
for (const auto& row : block.data) {
if ((row.fields.size() != 3U && row.fields.size() != 4U) ||
row.fields[0].empty()) {
diagnostics_.InputFailure(
"invalid-data-arity", row.location, block.canonical_name, "",
"Thermal boundaries require target, DOF, DOF, value.");
return;
}
std::int64_t first = 0;
std::int64_t last = 0;
if (!diagnostics_.ParseInteger(row.fields[1], first, row.location,
block.canonical_name, true) ||
!diagnostics_.ParseInteger(row.fields[2], last, row.location,
block.canonical_name, true) ||
first != last || (first != 11 && first != 15)) {
diagnostics_.InputFailure("invalid-dof", row.location,
block.canonical_name, row.fields[0],
"Thermal boundaries support DOF 11 or 15.");
return;
}
double value = 0.0;
if (row.fields.size() == 4U &&
!diagnostics_.ParseThermalDouble(row.fields[3], value, row.location,
block.canonical_name))
return;
destination.push_back(
{row.fields[0], static_cast<int>(first), value, row.location});
}
}
void StepDefinitionCandidates::ParseBoundary(
const KeywordBlock& block,
std::vector<PrescribedDisplacementDefinition>& destination) {
const bool dynamic_input = std::any_of(
input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) {
return candidate.canonical_name == "DYNAMIC";
});
if (!diagnostics_.ValidateParameters(block, {},
dynamic_input
? "unsupported-dynamic-boundary"
: "invalid-keyword-parameter") ||
block.data.empty()) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure("invalid-data-arity", block.location,
block.canonical_name, "",
"BOUNDARY requires one or more data rows.");
}
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U && row.fields.size() != 4U) {
diagnostics_.InputFailure(
"invalid-data-arity", row.location, block.canonical_name, "",
"BOUNDARY rows require target, first DOF, last DOF, and "
"optional value.");
return;
}
if (row.fields[0].empty()) {
diagnostics_.InputFailure("unresolved-reference", row.location,
block.canonical_name, "",
"A boundary target cannot be empty.");
return;
}
std::int64_t first = 0;
std::int64_t last = 0;
if (!diagnostics_.ParseInteger(row.fields[1], first, row.location,
block.canonical_name, true) ||
!diagnostics_.ParseInteger(row.fields[2], last, row.location,
block.canonical_name, true)) {
return;
}
if (first < 1 || first > 6 || last < first || last > 6) {
diagnostics_.InputFailure(
"invalid-dof", row.location, block.canonical_name, row.fields[0],
"Boundary DOFs must be an ordered range in 1..6.");
return;
}
double value = 0.0;
if (row.fields.size() == 4U &&
!(model_element_family_ == ElementFamily::kSolid &&
row.fields[3].empty()) &&
!diagnostics_.ParseDouble(row.fields[3], value, row.location,
block.canonical_name)) {
return;
}
destination.push_back({row.fields[0], static_cast<int>(first),
static_cast<int>(last), value, row.location});
}
}
void StepDefinitionCandidates::ParseCload(const KeywordBlock& block,
std::vector<NodalLoad>& loads) {
if (step_.dynamic && model_element_family_ != ElementFamily::kSolid &&
!block.parameters.empty()) {
diagnostics_.InputFailure(
"unsupported-dynamic-load", block.location, block.canonical_name, "",
"Dynamic CLOAD does not support keyword parameters.");
return;
}
const bool solid_dynamic =
step_.dynamic && model_element_family_ == ElementFamily::kSolid;
if (!diagnostics_.ValidateParameters(
block, solid_dynamic ? std::vector<std::string_view>{"AMPLITUDE"}
: std::vector<std::string_view>{}) ||
block.data.empty()) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure("invalid-data-arity", block.location,
block.canonical_name, "",
"CLOAD requires one or more rows.");
}
return;
}
std::optional<EntityIndex> amplitude_index;
std::string amplitude_name;
if (diagnostics_.Parameter(block, "AMPLITUDE")) {
const auto* name = diagnostics_.RequiredParameterValue(block, "AMPLITUDE");
if (!name) return;
amplitude_name = *name;
for (std::size_t index = 0U; index < definition_.amplitudes.size();
++index) {
if (AsciiCaseInsensitiveEquals(definition_.amplitudes[index].name_raw,
*name)) {
amplitude_index = static_cast<EntityIndex>(index);
break;
}
}
if (!amplitude_index) {
diagnostics_.InputFailure(
"unresolved-amplitude", block.location, block.canonical_name, *name,
"CLOAD amplitude must resolve to a model-level curve.");
return;
}
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U || row.fields[0].empty()) {
diagnostics_.InputFailure("invalid-data-arity", row.location,
block.canonical_name, "",
"CLOAD rows require target, DOF, magnitude.");
return;
}
std::int64_t dof = 0;
double magnitude = 0.0;
if (!diagnostics_.ParseInteger(row.fields[1], dof, row.location,
block.canonical_name, true) ||
!diagnostics_.ParseDouble(row.fields[2], magnitude, row.location,
block.canonical_name)) {
return;
}
if (dof < 1 || dof > 6) {
diagnostics_.InputFailure("invalid-dof", row.location,
block.canonical_name, row.fields[0],
"CLOAD DOF must be in 1..6.");
return;
}
if ((step_.dynamic || model_element_family_ == ElementFamily::kSolid) &&
dof > 3) {
diagnostics_.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, amplitude_index, amplitude_name,
block.location});
}
}
void StepDefinitionCandidates::ParseDsload(const KeywordBlock& block) {
if (!diagnostics_.ValidateParameters(block, {},
"unsupported-dsload-parameter") ||
block.data.empty()) {
if (!diagnostics_.HasFailure()) {
diagnostics_.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()) {
diagnostics_.InputFailure("invalid-data-arity", row.location,
block.canonical_name, "",
"DSLOAD rows require surface, P, magnitude.");
return;
}
if (!IsPressureNameToken(row.fields[0])) {
diagnostics_.InputFailure(
"invalid-surface-name", row.location, block.canonical_name,
row.fields[0],
"DSLOAD surface names must use the approved ASCII name "
"syntax.");
return;
}
if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) {
diagnostics_.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 StepDefinitionCandidates::ParseStepStart(const KeywordBlock& block,
bool model_ready) {
if (step_seen_) {
if (step_.frequency || step_.dynamic) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"The frequency subset accepts exactly one procedure step.");
} else {
const auto* nlgeom = diagnostics_.Parameter(block, "NLGEOM");
const bool nonlinear_second_step =
nlgeom != nullptr && nlgeom->value &&
AsciiCaseInsensitiveEquals(*nlgeom->value, "YES");
if (step_.nlgeom || nonlinear_second_step) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"The nonlinear static subset accepts exactly one "
"procedure step.");
} else {
diagnostics_.InputFailure("unsupported-multiple-step", block.location,
block.canonical_name, "",
"V0 accepts exactly one analysis step.");
}
}
return;
}
if (!model_ready) {
diagnostics_.InvalidKeywordLocation(
block,
"The sole step follows the complete assembly and "
"material definitions.");
return;
}
if (!diagnostics_.ValidateParameters(
block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) ||
!diagnostics_.RequireNoData(block)) {
return;
}
StaticProcedureKind procedure_kind =
StaticProcedureKind::kLinearSmallDisplacement;
bool nlgeom = false;
const auto* nlgeom_parameter = diagnostics_.Parameter(block, "NLGEOM");
if (nlgeom_parameter != nullptr) {
if (!nlgeom_parameter->value || nlgeom_parameter->value->empty()) {
diagnostics_.InputFailure(
"invalid-keyword-parameter", block.location, block.canonical_name,
"NLGEOM", "NLGEOM requires YES or NO in the approved subset.");
return;
}
if (AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "YES")) {
if (model_element_family_ != ElementFamily::kShell &&
model_element_family_ != ElementFamily::kSolid) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, *nlgeom_parameter->value,
"NLGEOM=YES requires a supported Shell or Solid model.");
return;
}
procedure_kind = StaticProcedureKind::kNonlinearFullNewton;
nlgeom = true;
} else if (!AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "NO")) {
diagnostics_.InputFailure("unsupported-analysis-procedure",
block.location, block.canonical_name,
*nlgeom_parameter->value,
"NLGEOM must select YES or NO.");
return;
}
}
const auto* perturbation = diagnostics_.Parameter(block, "PERTURBATION");
if (perturbation != nullptr && perturbation->value) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location, block.canonical_name,
"PERTURBATION", "PERTURBATION is accepted only as a value-less flag.");
return;
}
const auto* name = diagnostics_.Parameter(block, "NAME");
if (name != nullptr && (!name->value || name->value->empty())) {
diagnostics_.InputFailure(
"invalid-keyword-parameter", block.location, block.canonical_name,
"NAME", "STEP NAME must not be empty when it is present.");
return;
}
const auto* increment_limit = diagnostics_.Parameter(block, "INC");
std::uint32_t maximum_increments = 100U;
if (increment_limit != nullptr) {
if (!increment_limit->value || increment_limit->value->empty()) {
diagnostics_.InputFailure("invalid-dynamic-data", block.location,
block.canonical_name, "INC",
"INC requires a positive uint32 value.");
return;
}
std::int64_t parsed_limit = 0;
if (!diagnostics_.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 (!diagnostics_.HasFailure()) {
diagnostics_.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_ = {};
step_.location = block.location;
step_.name_raw = name == nullptr ? "Step-1" : *name->value;
step_.name_canonical = AsciiUppercase(step_.name_raw);
step_.procedure_kind = procedure_kind;
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;
}
void StepDefinitionCandidates::ParseStepBlock(const KeywordBlock& block) {
if (model_element_family_ == ElementFamily::kSolid &&
(block.canonical_name == "STATIC" ||
block.canonical_name == "FREQUENCY" ||
block.canonical_name == "DYNAMIC") &&
(step_.has_static || step_.frequency || step_.dynamic)) {
diagnostics_.InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, step_.name_raw,
"Solid steps accept exactly one procedure.");
return;
}
if (model_element_family_ == ElementFamily::kThermalShell &&
(block.canonical_name == "STATIC" ||
block.canonical_name == "FREQUENCY" ||
block.canonical_name == "DYNAMIC")) {
diagnostics_.InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, step_.name_raw,
"DS4 supports only one HEAT TRANSFER procedure.");
return;
}
if (block.canonical_name == "END STEP") {
active_output_ = false;
if (!diagnostics_.ValidateParameters(block, {}) ||
!diagnostics_.RequireNoData(block)) {
return;
}
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
!step_.has_heat_transfer) {
diagnostics_.InvalidKeywordLocation(
block, "The sole step requires one supported procedure keyword.");
return;
}
in_step_ = false;
return;
}
if (block.canonical_name == "STEP") {
diagnostics_.InputFailure("unsupported-multiple-step", block.location,
block.canonical_name, "",
"A second or nested step is unsupported.");
return;
}
if (block.canonical_name == "HEAT TRANSFER") {
active_output_ = false;
if (step_.has_static || step_.frequency || step_.dynamic ||
step_.has_heat_transfer || !step_.boundaries.empty() ||
!step_.loads.empty() || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block, "HEAT TRANSFER must be the first procedure.");
return;
}
ParseHeatTransfer(block);
} else if (block.canonical_name == "STATIC") {
active_output_ = false;
if (!step_.boundaries.empty() || !step_.loads.empty() ||
!step_.surface_pressure_loads.empty() || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block, "STATIC must be the first keyword in the sole step.");
return;
}
ParseStatic(block);
} else if (block.canonical_name == "FREQUENCY") {
active_output_ = false;
if (step_.has_static || step_.frequency || !step_.boundaries.empty() ||
!step_.loads.empty() || !step_.surface_pressure_loads.empty() ||
step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block, "FREQUENCY must be the first procedure in the sole step.");
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_) {
diagnostics_.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_.dynamic &&
!step_.has_heat_transfer) ||
step_load_seen_ || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block,
"Step boundaries follow STATIC and precede loads "
"and no-op requests.");
return;
}
if (step_.dynamic && !block.parameters.empty()) {
diagnostics_.InputFailure(
"unsupported-dynamic-boundary", block.location, block.canonical_name,
"", "Dynamic BOUNDARY does not support keyword parameters.");
return;
}
if (step_.has_heat_transfer) {
ParseThermalBoundary(block, step_.thermal_boundaries);
} else {
ParseBoundary(block, step_.boundaries);
}
if (!diagnostics_.HasFailure() && step_.frequency) {
ValidateEigenBoundaries(step_.boundaries);
}
if (!diagnostics_.HasFailure() && step_.dynamic) {
ValidateDynamicBoundaries(step_.boundaries);
}
} else if (block.canonical_name == "CLOAD") {
active_output_ = false;
if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block,
"CLOAD follows STATIC and boundaries and precedes no-op requests.");
return;
}
ParseCload(block, step_.loads);
if (!diagnostics_.HasFailure()) {
step_.next_load_ordinal += block.data.size();
}
step_load_seen_ = !diagnostics_.HasFailure();
} else if (block.canonical_name == "CFLUX") {
active_output_ = false;
if (!step_.has_heat_transfer || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block, "CFLUX follows HEAT TRANSFER and boundaries.");
return;
}
ParseCflux(block);
step_load_seen_ = !diagnostics_.HasFailure();
} else if (block.canonical_name == "DSLOAD") {
active_output_ = false;
if (!step_.has_static || step_.dynamic || step_no_op_seen_) {
diagnostics_.InvalidKeywordLocation(
block,
"DSLOAD follows STATIC and boundaries and "
"precedes no-op requests.");
return;
}
ParseDsload(block);
step_load_seen_ = !diagnostics_.HasFailure();
} else if (block.canonical_name == "RESTART") {
active_output_ = false;
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
!step_.has_heat_transfer) {
diagnostics_.InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
step_no_op_seen_ = true;
if (step_.frequency || step_.has_heat_transfer ||
model_element_family_ == ElementFamily::kSolid) {
const auto* write = diagnostics_.Parameter(block, "WRITE");
const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY");
if (!diagnostics_.ValidateParameters(block, {"WRITE", "FREQUENCY"}) ||
write == nullptr || write->value || frequency == nullptr ||
!frequency->value || *frequency->value != "0") {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Supported RESTART no-op requires WRITE, FREQUENCY=0.");
}
return;
}
}
if (diagnostics_.RequireNoData(block)) {
AddIgnoredWarning(block);
}
} else if (block.canonical_name == "OUTPUT") {
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
!step_.has_heat_transfer) {
diagnostics_.InvalidKeywordLocation(
block, "Step no-op requests follow STATIC, boundaries, and loads.");
return;
}
step_no_op_seen_ = true;
ParseOutputRoot(block);
} else if (block.canonical_name == "NODE OUTPUT" ||
block.canonical_name == "ELEMENT OUTPUT" ||
block.canonical_name == "CONTACT OUTPUT" ||
block.canonical_name == "ENERGY OUTPUT") {
ParseOutputChild(block);
} else {
active_output_ = false;
if (step_.procedure_kind == StaticProcedureKind::kNonlinearFullNewton &&
!step_.has_static) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"The nonlinear step supports only the STATIC procedure.");
return;
}
diagnostics_.RejectUnknown(block,
model_element_family_ == ElementFamily::kShell);
}
}
bool StepDefinitionCandidates::ParseFrequencyModeCount(
const std::string& text, std::int64_t& value,
const SourceLocation& location) {
if (text.empty() ||
!std::all_of(text.begin(), text.end(), [](char character) {
return character >= '0' && character <= '9';
})) {
return diagnostics_.InputFailure(
"invalid-frequency-data", location, "FREQUENCY", text,
"The requested mode count must be positive decimal digits.");
}
const auto parsed =
std::from_chars(text.data(), text.data() + text.size(), value);
if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() ||
value <= 0) {
return diagnostics_.InputFailure(
"invalid-frequency-data", location, "FREQUENCY", text,
"The requested mode count must be a positive int64 value.");
}
return true;
}
void StepDefinitionCandidates::ParseFrequency(const KeywordBlock& block) {
if (step_.nlgeom || (step_.perturbation_explicit && step_.has_static) ||
!diagnostics_.ValidateParameters(
block, {"EIGENSOLVER", "SIM", "ACOUSTIC COUPLING", "NORMALIZATION"},
"unsupported-frequency-option")) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"FREQUENCY requires the original linear configuration.");
}
return;
}
if (block.data.size() != 1U || block.data[0].fields.empty() ||
block.data[0].fields.size() > 7U) {
diagnostics_.InputFailure(
"invalid-frequency-data", block.location, block.canonical_name, "",
"FREQUENCY requires one data row with one to seven fields.");
return;
}
for (std::size_t field = 1U; field < block.data[0].fields.size(); ++field) {
if (!block.data[0].fields[field].empty()) {
diagnostics_.InputFailure(
"invalid-frequency-data", block.data[0].location,
block.canonical_name, std::to_string(field + 1U),
"Only the requested mode-count field may be non-empty.");
return;
}
}
FrequencyStepDefinition frequency{step_.name_raw,
step_.name_canonical,
step_.location,
block.location,
block.data[0].location,
0,
step_.nlgeom_explicit,
step_.perturbation_explicit,
false,
false,
false,
false,
FrequencyEigensolver::kLanczos,
FrequencyNormalization::kMass,
{}};
if (!ParseFrequencyModeCount(block.data[0].fields[0],
frequency.requested_mode_count,
block.data[0].location)) {
return;
}
const auto* eigensolver = diagnostics_.Parameter(block, "EIGENSOLVER");
if (eigensolver != nullptr) {
if (!eigensolver->value ||
!AsciiCaseInsensitiveEquals(*eigensolver->value, "LANCZOS")) {
diagnostics_.InputFailure("unsupported-frequency-option", block.location,
block.canonical_name, "EIGENSOLVER",
"Only EIGENSOLVER=LANCZOS is accepted.");
return;
}
frequency.eigensolver_explicit = true;
}
const auto* sim = diagnostics_.Parameter(block, "SIM");
if (sim != nullptr) {
if (sim->value && !AsciiCaseInsensitiveEquals(*sim->value, "YES")) {
diagnostics_.InputFailure("unsupported-frequency-option", block.location,
block.canonical_name, "SIM",
"SIM accepts only a flag or YES.");
return;
}
frequency.sim_explicit = true;
}
const auto* acoustic = diagnostics_.Parameter(block, "ACOUSTIC COUPLING");
if (acoustic != nullptr) {
if (!acoustic->value ||
!AsciiCaseInsensitiveEquals(*acoustic->value, "ON")) {
diagnostics_.InputFailure("unsupported-frequency-option", block.location,
block.canonical_name, "ACOUSTIC COUPLING",
"Only ACOUSTIC COUPLING=ON is accepted.");
return;
}
frequency.acoustic_coupling_explicit = true;
}
const auto* normalization = diagnostics_.Parameter(block, "NORMALIZATION");
if (normalization != nullptr) {
if (!normalization->value ||
!AsciiCaseInsensitiveEquals(*normalization->value, "MASS")) {
diagnostics_.InputFailure("unsupported-frequency-option", block.location,
block.canonical_name, "NORMALIZATION",
"Only NORMALIZATION=MASS is accepted.");
return;
}
frequency.normalization_explicit = true;
}
step_.frequency = std::move(frequency);
definition_.warnings.push_back(
{Severity::kWarning, "source-eigensolver-mapped-to-feast", block.location,
block.canonical_name, step_.name_raw,
"The accepted source eigensolver is mapped to the FEAST backend."});
if (step_.frequency->sim_explicit) {
definition_.warnings.push_back(
{Severity::kWarning, "source-sim-option-not-applied", block.location,
block.canonical_name, step_.name_raw,
"The accepted source SIM option has no FESA runtime effect."});
}
if (step_.frequency->acoustic_coupling_explicit) {
definition_.warnings.push_back(
{Severity::kWarning, "acoustic-option-inapplicable-structural-only",
block.location, block.canonical_name, step_.name_raw,
"Acoustic coupling is inapplicable to this structural-only model."});
}
}
void StepDefinitionCandidates::ValidateEigenBoundaries(
const std::vector<PrescribedDisplacementDefinition>& boundaries) {
for (const auto& boundary : boundaries) {
if (boundary.value != 0.0) {
diagnostics_.InputFailure(
"unsupported-eigen-boundary", boundary.location, "BOUNDARY",
boundary.target, "Frequency boundaries must prescribe exact zero.");
return;
}
}
}
void StepDefinitionCandidates::ValidateDynamicBoundaries(
const std::vector<PrescribedDisplacementDefinition>& boundaries) {
for (const auto& boundary : boundaries) {
if (boundary.value != 0.0) {
diagnostics_.InputFailure(
"unsupported-dynamic-boundary", boundary.location, "BOUNDARY",
boundary.target,
"Dynamic boundaries must be homogeneous and constant.");
return;
}
}
}
void StepDefinitionCandidates::ParseDynamic(const KeywordBlock& block) {
if (step_.perturbation_explicit ||
(model_element_family_ != ElementFamily::kSolid &&
(!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) ||
!diagnostics_.ValidateParameters(block, {"DIRECT"},
"unsupported-dynamic-parameter")) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"DYNAMIC requires nonlinear Shell or linear/nonlinear "
"Solid geometry.");
}
return;
}
if (model_element_family_ == ElementFamily::kSolid &&
block.data.size() == 1U && block.data[0].fields.size() > 3U) {
diagnostics_.InputFailure(
"invalid-dynamic-data", block.data[0].location, block.canonical_name,
"", "Solid DYNAMIC requires h,T with at most one blank field.");
return;
}
const auto* direct = diagnostics_.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(); })) {
diagnostics_.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 (!diagnostics_.HasFailure()) {
diagnostics_.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) {
diagnostics_.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) {
diagnostics_.InputFailure(
"invalid-dynamic-schedule", block.data[0].location,
block.canonical_name, "",
"DYNAMIC,DIRECT increment count cannot exceed STEP INC.");
return;
}
dynamic.nlgeom = step_.nlgeom;
step_.dynamic = std::move(dynamic);
}
void StepDefinitionCandidates::ParseHeatTransfer(const KeywordBlock& block) {
if (model_element_family_ != ElementFamily::kThermalShell || step_.nlgeom ||
step_.perturbation_explicit ||
!diagnostics_.ValidateParameters(block, {"END"},
"unsupported-thermal-option") ||
block.data.size() != 1U) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"HEAT TRANSFER requires the DS4 thermal subset.");
return;
}
const auto* end = diagnostics_.Parameter(block, "END");
if (end != nullptr &&
(!end->value || !AsciiCaseInsensitiveEquals(*end->value, "PERIOD"))) {
diagnostics_.InputFailure("unsupported-thermal-option", block.location,
block.canonical_name, "END",
"HEAT TRANSFER accepts only END=PERIOD.");
return;
}
if (block.data[0].fields.size() > 5U) {
diagnostics_.InputFailure(
"invalid-thermal-time-grid", block.data[0].location,
block.canonical_name, "",
"HEAT TRANSFER permits at most three trailing empty fields.");
return;
}
const auto fields = WithoutTrailingEmpty(block.data[0].fields);
if (fields.size() != 2U) {
diagnostics_.InputFailure(
"invalid-thermal-time-grid", block.data[0].location,
block.canonical_name, "",
"HEAT TRANSFER requires increment and time period.");
return;
}
if (!diagnostics_.ParseThermalDouble(fields[0], step_.thermal_increment,
block.data[0].location,
block.canonical_name) ||
!diagnostics_.ParseThermalDouble(fields[1], step_.thermal_period,
block.data[0].location,
block.canonical_name) ||
!(step_.thermal_increment > 0.0) || !(step_.thermal_period > 0.0)) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure(
"invalid-thermal-time-grid", block.data[0].location,
block.canonical_name, "",
"Thermal increment and period must be finite and positive.");
return;
}
const auto grid = ThermalTimeGrid::Create(
step_.thermal_period, step_.thermal_increment, step_.maximum_increments);
if (!grid.HasValue()) {
auto diagnostic = grid.GetStatus().Diagnostics().front();
diagnostic.location = block.data[0].location;
diagnostic.keyword = block.canonical_name;
diagnostic.entity_identity = step_.name_raw;
diagnostics_.Fail(
grid.GetStatus().Category().value_or(FailureCategory::kInput),
std::move(diagnostic.code), diagnostic.location,
std::move(diagnostic.keyword), std::move(diagnostic.entity_identity),
std::move(diagnostic.message));
return;
}
step_.has_heat_transfer = true;
}
void StepDefinitionCandidates::ParseCflux(const KeywordBlock& block) {
if (!diagnostics_.ValidateParameters(block, {}) || block.data.empty()) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure("invalid-data-arity", block.location,
block.canonical_name, "",
"CFLUX requires target, DOF, power rows.");
return;
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U || row.fields[0].empty()) {
diagnostics_.InputFailure("invalid-data-arity", row.location,
block.canonical_name, "",
"CFLUX rows require target, DOF, power.");
return;
}
std::int64_t dof = 0;
double power = 0.0;
if (!diagnostics_.ParseInteger(row.fields[1], dof, row.location,
block.canonical_name, true) ||
!diagnostics_.ParseThermalDouble(row.fields[2], power, row.location,
block.canonical_name) ||
(dof != 11 && dof != 15)) {
diagnostics_.InputFailure("invalid-dof", row.location,
block.canonical_name, row.fields[0],
"CFLUX supports DOF 11 or 15.");
return;
}
step_.thermal_fluxes.push_back(
{row.fields[0], static_cast<int>(dof), power, row.location});
}
}
void StepDefinitionCandidates::ParseStatic(const KeywordBlock& block) {
if (model_element_family_ == ElementFamily::kSolid &&
diagnostics_.Parameter(block, "DIRECT")) {
if (step_.perturbation_explicit || step_.has_static || step_.frequency ||
step_.dynamic) {
diagnostics_.InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, step_.name_raw,
"STATIC DIRECT requires one non-perturbation procedure.");
return;
}
if (!diagnostics_.ValidateParameters(block, {"DIRECT"})) return;
const auto* direct = diagnostics_.Parameter(block, "DIRECT");
if (direct->value || block.data.size() != 1U ||
(block.data[0].fields.size() < 2U || block.data[0].fields.size() > 3U ||
std::any_of(block.data[0].fields.begin() + 2,
block.data[0].fields.end(),
[](const auto& field) { return !field.empty(); }))) {
diagnostics_.InputFailure("invalid-static-data", block.location,
block.canonical_name, "DIRECT",
"STATIC DIRECT requires exactly h,T.");
return;
}
double h = 0.0;
double period = 0.0;
if (!diagnostics_.ParseDouble(block.data[0].fields[0], h,
block.data[0].location, "STATIC") ||
!diagnostics_.ParseDouble(block.data[0].fields[1], period,
block.data[0].location, "STATIC"))
return;
const double count = std::round(period / h);
if (!(h > 0.0) || !(period > 0.0) || !std::isfinite(count) || count < 1.0 ||
count > static_cast<double>(step_.maximum_increments) ||
h * count != period) {
diagnostics_.InputFailure(
"invalid-static-schedule", block.data[0].location,
block.canonical_name, "DIRECT",
"STATIC DIRECT requires positive exact N*h=T and N<=INC.");
return;
}
step_.static_values = {h, period, h, h};
step_.static_direct = true;
step_.static_increment_count = static_cast<std::uint32_t>(count);
step_.has_static = true;
return;
}
if (step_.perturbation_explicit || step_.frequency) {
diagnostics_.InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, "PERTURBATION",
"PERTURBATION is supported only with FREQUENCY.");
return;
}
if (step_.has_heat_transfer || step_.has_static ||
!diagnostics_.ValidateParameters(block, {}) || block.data.size() != 1U ||
block.data[0].fields.size() != 4U) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
step_.nlgeom ? "invalid-nonlinear-static-data"
: "invalid-static-data",
block.location, block.canonical_name, "",
"STATIC requires exactly one row of four values.");
}
return;
}
for (std::size_t field = 0U; field < 4U; ++field) {
const std::string& text = block.data[0].fields[field];
const bool parsed = step_.nlgeom
? ParseNonlinearStaticDouble(
text, step_.static_values[field],
block.data[0].location, block.canonical_name)
: diagnostics_.ParseDouble(
text, step_.static_values[field],
block.data[0].location, block.canonical_name);
if (!parsed) {
return;
}
if (step_.static_values[field] <= 0.0) {
diagnostics_.InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data"
: "invalid-static-data",
block.data[0].location, block.canonical_name,
"", "All four STATIC fields must be positive.");
return;
}
}
if (step_.static_values[2] > step_.static_values[3]) {
diagnostics_.InputFailure(
step_.nlgeom ? "invalid-nonlinear-static-data" : "invalid-static-data",
block.data[0].location, block.canonical_name, "",
"STATIC minimum increment cannot exceed maximum increment.");
return;
}
if (step_.nlgeom && step_.static_values[1] != 1.0) {
diagnostics_.InputFailure(
"invalid-nonlinear-static-data", block.data[0].location,
block.canonical_name, "",
"The nonlinear STATIC time period must be exactly 1.0.");
return;
}
step_.has_static = true;
}
void StepDefinitionCandidates::ParseOutputRoot(const KeywordBlock& block) {
if (step_.has_heat_transfer) {
const auto* field = diagnostics_.Parameter(block, "FIELD");
const auto* history = diagnostics_.Parameter(block, "HISTORY");
const auto* variable = diagnostics_.Parameter(block, "VARIABLE");
const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY");
const bool valid_field =
field && !field->value && variable && variable->value &&
AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT");
const bool valid_history = history && !history->value && frequency &&
frequency->value && *frequency->value == "0";
if (!diagnostics_.RequireNoData(block)) return;
if (block.parameters.size() != 2U || (!valid_field && !valid_history)) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Thermal OUTPUT accepts FIELD, VARIABLE=PRESELECT or "
"HISTORY, FREQUENCY=0.");
return;
}
AddIgnoredWarning(block);
return;
}
if (!diagnostics_.ValidateParameters(
block, (step_.dynamic || step_.has_heat_transfer)
? std::vector<std::string_view>{"FIELD", "HISTORY",
"VARIABLE", "FREQUENCY"}
: std::vector<std::string_view>{"FIELD", "HISTORY",
"VARIABLE"})) {
return;
}
const auto* field = diagnostics_.Parameter(block, "FIELD");
const auto* history = diagnostics_.Parameter(block, "HISTORY");
const auto* variable = diagnostics_.Parameter(block, "VARIABLE");
if ((field == nullptr) == (history == nullptr) ||
(field != nullptr && field->value) ||
(history != nullptr && history->value)) {
diagnostics_.InputFailure(
step_.nlgeom ? "invalid-keyword-parameter" : "unsupported-keyword",
block.location, block.canonical_name, "",
"OUTPUT must select exactly FIELD or HISTORY.");
return;
}
if (model_element_family_ == ElementFamily::kSolid &&
((variable && (!variable->value || !AsciiCaseInsensitiveEquals(
*variable->value, "PRESELECT"))) ||
!diagnostics_.RequireNoData(block))) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name,
"VARIABLE", "Solid OUTPUT accepts only PRESELECT requests.");
return;
}
if (step_.nlgeom &&
(variable == nullptr || !variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) {
diagnostics_.InputFailure("invalid-keyword-parameter", block.location,
block.canonical_name, "VARIABLE",
"Nonlinear OUTPUT requires VARIABLE=PRESELECT.");
return;
}
if (step_.dynamic) {
const auto* frequency = diagnostics_.Parameter(block, "FREQUENCY");
if (!diagnostics_.ValidateParameters(
block, {"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) ||
variable == nullptr || !variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") ||
(frequency != nullptr &&
(!frequency->value || *frequency->value != "1")) ||
!diagnostics_.RequireNoData(block)) {
if (!diagnostics_.HasFailure()) {
diagnostics_.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"))) ||
!diagnostics_.RequireNoData(block))) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Frequency OUTPUT accepts only VARIABLE=PRESELECT.");
}
return;
}
active_output_ = true;
active_output_is_field_ = field != nullptr;
AddIgnoredWarning(block);
}
void StepDefinitionCandidates::ParseOutputChild(const KeywordBlock& block) {
if (step_.has_heat_transfer) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Thermal output children are outside the approved subset.");
return;
}
if (!active_output_) {
diagnostics_.InputFailure(
"invalid-keyword-location", block.location, block.canonical_name, "",
"Output variable keywords require an active OUTPUT request.");
return;
}
if (model_element_family_ == ElementFamily::kSolid && step_.dynamic) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Solid dynamic output children are outside the approved subset.");
return;
}
if (model_element_family_ == ElementFamily::kSolid && step_.has_static) {
const auto* variable = diagnostics_.Parameter(block, "VARIABLE");
const auto* directions = diagnostics_.Parameter(block, "DIRECTIONS");
const bool node = block.canonical_name == "NODE OUTPUT";
const bool element = block.canonical_name == "ELEMENT OUTPUT";
const bool contact = block.canonical_name == "CONTACT OUTPUT";
if (!active_output_is_field_ || (!node && !element && !contact) ||
!diagnostics_.ValidateParameters(
block, element
? std::vector<std::string_view>{"VARIABLE", "DIRECTIONS"}
: std::vector<std::string_view>{"VARIABLE"}) ||
(directions &&
(!directions->value ||
!AsciiCaseInsensitiveEquals(*directions->value, "YES"))) ||
(variable &&
(!variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
(contact && (!variable || !block.data.empty()))) {
if (!diagnostics_.HasFailure())
diagnostics_.InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"Solid static output child is unsupported.");
return;
}
for (const auto& row : block.data) {
for (const auto& field : row.fields) {
const std::string token = AsciiUppercase(field);
if (!(node ? (token == "U" || token == "RF")
: (token == "S" || token == "E"))) {
diagnostics_.InputFailure(
"unsupported-keyword", row.location, block.canonical_name, field,
"Solid output quantities must be physical supported "
"components.");
return;
}
}
}
}
if (step_.frequency) {
const std::string_view required_variable =
block.canonical_name == "NODE OUTPUT" ? "PRESELECT" : "ALL";
const auto* variable = diagnostics_.Parameter(block, "VARIABLE");
if ((block.canonical_name != "NODE OUTPUT" &&
block.canonical_name != "ENERGY OUTPUT") ||
(block.canonical_name == "NODE OUTPUT" && !active_output_is_field_) ||
(block.canonical_name == "ENERGY OUTPUT" && active_output_is_field_) ||
!diagnostics_.ValidateParameters(block, {"VARIABLE"}) ||
variable == nullptr || !variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, required_variable) ||
!diagnostics_.RequireNoData(block)) {
if (!diagnostics_.HasFailure()) {
diagnostics_.InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Frequency output child is outside the approved subset.");
}
return;
}
}
AddIgnoredWarning(block);
}
void StepDefinitionCandidates::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,
step_.dynamic ? "ignored-output-request" : "ignored-input-keyword",
block.location, block.canonical_name, "",
"The allowlisted Abaqus keyword is "
"ignored without semantic effect."});
}
std::optional<std::vector<EntityIndex>>
StepDefinitionCandidates::ResolveNodeTarget(
const SourceTargetResolver& resolver, const std::string& target,
const SourceLocation& location, const std::string& keyword) {
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
if (!resolved.HasValue()) {
diagnostics_.InputFailure(
"unresolved-reference", location, keyword, target,
"The boundary or load target must resolve unambiguously to one "
"node or one node set.");
return std::nullopt;
}
std::vector<EntityIndex> indices;
indices.reserve(resolved.Value().size());
for (const auto& entry : resolved.Value()) {
if (model_element_family_ == ElementFamily::kThermalShell &&
!thermal_active_nodes_[entry.entity_index]) {
diagnostics_.ModelFailure(
"inactive-thermal-target", location, keyword, target,
"Thermal targets must connect to an active DS4 element.");
return std::nullopt;
}
indices.push_back(entry.entity_index);
}
return indices;
}
void StepDefinitionCandidates::FinalizeStep() {
if (step_.has_heat_transfer) {
thermal_active_nodes_.assign(definition_.nodes.size(), false);
for (const auto& element : definition_.thermal_elements)
for (const auto node : element.node_indices)
thermal_active_nodes_[node] = true;
const SourceTargetIndex target_index = SourceTargetIndex::FromNodeDefinitions(definition_.nodes, definition_.node_sets);
const SourceTargetResolver resolver{target_index};
const auto append_boundaries =
[this, &resolver](
const std::vector<RawThermalBoundary>& rows,
std::map<std::pair<EntityIndex, int>, double>* values,
std::vector<ThermalBoundaryConditionDefinition>* out) {
for (const auto& row : rows) {
const auto nodes = ResolveNodeTarget(resolver, row.target,
row.location, "BOUNDARY");
if (!nodes) return false;
for (const EntityIndex node : *nodes) {
const auto key = std::make_pair(node, row.dof);
const auto existing = values->find(key);
if (existing != values->end() && existing->second != row.value) {
return diagnostics_.InputFailure(
"conflicting-boundary-condition", row.location, "BOUNDARY",
row.target,
"Expanded thermal boundaries prescribe "
"different values.");
}
(*values)[key] = row.value;
out->push_back({node, row.dof, row.value, row.location});
}
}
return true;
};
std::map<std::pair<EntityIndex, int>, double> model_values;
if (!append_boundaries(thermal_model_boundaries_, &model_values,
&definition_.thermal_model_boundaries))
return;
std::vector<ThermalBoundaryConditionDefinition> step_boundaries;
std::map<std::pair<EntityIndex, int>, double> step_values;
if (!append_boundaries(step_.thermal_boundaries, &step_values,
&step_boundaries))
return;
std::map<EntityIndex, double> initial_values;
for (const auto& row : thermal_initial_conditions_) {
const auto nodes = ResolveNodeTarget(resolver, row.target, row.location,
"INITIAL CONDITIONS");
if (!nodes) return;
for (const EntityIndex node : *nodes) {
const auto inserted = initial_values.emplace(node, row.value);
if (!inserted.second && inserted.first->second != row.value) {
diagnostics_.InputFailure(
"conflicting-initial-temperature", row.location,
"INITIAL CONDITIONS", row.target,
"Expanded initial temperatures prescribe different values.");
return;
}
definition_.thermal_initial_conditions.push_back(
{node, row.value, row.location});
}
}
std::vector<ThermalFluxLoadDefinition> loads;
for (const auto& row : step_.thermal_fluxes) {
const auto nodes =
ResolveNodeTarget(resolver, row.target, row.location, "CFLUX");
if (!nodes) return;
for (const EntityIndex node : *nodes) {
loads.push_back({node, row.dof, row.power, row.location, loads.size()});
}
}
definition_.thermal_step = {step_.name_raw,
step_.thermal_increment,
step_.thermal_period,
step_.maximum_increments,
false,
std::move(step_boundaries),
std::move(loads),
step_.location};
return;
}
std::vector<PrescribedDisplacementDefinition> boundaries = model_boundaries_;
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
step_.boundaries.end());
if (step_.frequency) {
ValidateEigenBoundaries(boundaries);
if (diagnostics_.HasFailure()) {
return;
}
}
if (step_.dynamic) {
ValidateDynamicBoundaries(boundaries);
if (diagnostics_.HasFailure()) {
return;
}
}
const SourceTargetIndex target_index = SourceTargetIndex::FromNodeDefinitions(definition_.nodes, definition_.node_sets);
const SourceTargetResolver target_resolver{target_index};
if (model_element_family_ == ElementFamily::kSolid) {
std::vector<PrescribedDisplacementDefinition> physical;
for (auto boundary : boundaries) {
if (!ResolveNodeTarget(target_resolver, boundary.target,
boundary.location, "BOUNDARY"))
return;
if (boundary.last_dof > 3) {
if (boundary.value != 0.0) {
diagnostics_.InputFailure(
"unsupported-solid-rotation-boundary", boundary.location,
"BOUNDARY", boundary.target,
"Solid rotational boundaries must be exact zero to be "
"inactive.");
return;
}
definition_.warnings.push_back(
{Severity::kWarning, "inactive-solid-rotation-boundary",
boundary.location, "BOUNDARY", boundary.target,
"Exact-zero rotational boundary DOFs " +
std::to_string(std::max(4, boundary.first_dof)) + ".." +
std::to_string(boundary.last_dof) +
" are inactive; Solid equations contain translations "
"only."});
boundary.last_dof = 3;
}
if (boundary.first_dof <= boundary.last_dof)
physical.push_back(std::move(boundary));
}
boundaries = std::move(physical);
std::vector<bool> active(definition_.nodes.size(), false);
for (const auto& element : definition_.solid_elements)
for (const auto node : element.node_indices) active[node] = true;
for (std::size_t node = 0U; node < active.size(); ++node) {
if (!active[node]) {
diagnostics_.ModelFailure(
"unused-solid-node", definition_.nodes[node].location, "NODE",
definition_.nodes[node].source_id.source_label_text,
"Every Solid node must connect to a C3D8 element.");
return;
}
}
}
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
for (const auto& boundary : boundaries) {
auto target = ResolveNodeTarget(target_resolver, boundary.target,
boundary.location, "BOUNDARY");
if (!target) {
return;
}
for (const auto node : *target) {
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
const auto key = std::make_pair(node, dof);
const auto existing = prescribed_values.find(key);
if (existing != prescribed_values.end() &&
existing->second != boundary.value) {
diagnostics_.InputFailure(
"conflicting-boundary-condition", boundary.location, "BOUNDARY",
boundary.target,
"Expanded boundary rows prescribe different values to "
"one node/DOF.");
return;
}
prescribed_values[key] = boundary.value;
}
}
}
for (const auto& load : step_.loads) {
if (!ResolveNodeTarget(target_resolver, load.target, load.location,
"CLOAD")) {
return;
}
}
if (step_.frequency) {
step_.frequency->boundaries = std::move(boundaries);
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) {
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()) {
diagnostics_.InputFailure(
"missing-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, AsciiUppercase(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(
{model_element_family_ == ElementFamily::kSolid ? step_.name_raw
: "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});
definition_.steps.back().direct = step_.static_direct;
definition_.steps.back().increment_count = step_.static_increment_count;
definition_.steps.back().maximum_increments = step_.maximum_increments;
}
Status StepDefinitionMapper::Map(DomainMappingContext& context) const {
return context.FinalizeStep();
}
} // namespace fesa::abaqus_internal