#include "io/abaqus/step_definition_mapper.h" #include #include #include #include #include #include #include #include #include #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 first_surface, const std::optional& 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, 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& 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(first), value, row.location}); } } void StepDefinitionCandidates::ParseBoundary( const KeywordBlock& block, std::vector& 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(first), static_cast(last), value, row.location}); } } void StepDefinitionCandidates::ParseCload(const KeywordBlock& block, std::vector& 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{"AMPLITUDE"} : std::vector{}) || 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 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(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(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::numeric_limits::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(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& 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& 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(std::numeric_limits::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(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(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(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(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{"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"} : std::vector{"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{"VARIABLE", "DIRECTIONS"} : std::vector{"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> 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 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& rows, std::map, double>* values, std::vector* 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, double> model_values; if (!append_boundaries(thermal_model_boundaries_, &model_values, &definition_.thermal_model_boundaries)) return; std::vector step_boundaries; std::map, double> step_values; if (!append_boundaries(step_.thermal_boundaries, &step_values, &step_boundaries)) return; std::map 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 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 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 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 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, 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 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( 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