1682 lines
70 KiB
C++
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
|