#include "io/abaqus/domain_mapping_context.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/thermal_time_grid.h" #include "fesa/core/ascii.h" #include "fesa/math/vector3.h" #include "fesa/model/shell_geometry.h" #include "fesa/model/source_target_resolver.h" namespace fesa::abaqus_internal { namespace { std::vector WithoutTrailingEmpty(std::vector fields) { while (!fields.empty() && fields.back().empty()) { fields.pop_back(); } return fields; } std::string CanonicalizeAscii(std::string value) { for (char& character : value) { if (character >= 'a' && character <= 'z') { character = static_cast(character - 'a' + 'A'); } } return value; } struct RawNode { std::int64_t label; std::string label_text; std::array coordinates; SourceLocation location; }; struct RawElement { enum class Type { kB33, kS4, kS4r, kDs4, kC3d8 }; std::int64_t label; std::string label_text; Type type; std::vector node_labels; SourceLocation location; }; enum class ElementFamily { kBeam, kShell, kThermalShell, kSolid }; struct RawSet { std::string name; std::vector members; SourceLocation location; }; struct RawSection { std::string element_set_name; std::string material_name; std::array properties; std::array first_axis; std::vector> section_points; SourceLocation location; }; struct RawShellSection { std::string element_set_name; std::string material_name; double thickness; SourceLocation location; }; struct RawSolidSection { std::string element_set_name; std::string material_name; SourceLocation location; }; struct RawPart { std::string name; SourceLocation location; std::vector nodes; std::vector elements; std::vector node_sets; std::vector element_sets; std::vector sections; std::vector shell_sections; std::vector solid_sections; }; struct RawInstance { std::string name; std::string part_name; SourceLocation location; }; struct RawAssemblySet { bool is_node_set; std::string name; std::string instance_name; std::vector members; SourceLocation location; }; struct RawSurfaceMember { std::string target; ShellSurfaceSide side; SourceLocation location; std::size_t ordinal; }; struct RawElementSurface { std::string name; SourceLocation location; std::size_t declaration_ordinal; std::vector members; }; struct RawSurfacePressureLoad { std::string surface_name; double magnitude; std::string magnitude_raw; SourceLocation keyword_location; SourceLocation data_location; std::size_t data_ordinal; std::size_t load_ordinal; }; struct RawMaterial { std::string name; double youngs_modulus{0.0}; double poisson_ratio{0.0}; bool has_elastic{false}; std::optional density; std::optional conductivity; std::optional specific_heat; SourceLocation location; SourceLocation elastic_location; SourceLocation density_location; }; struct RawThermalBoundary { std::string target; int dof; double value; SourceLocation location; }; struct RawThermalInitial { std::string target; double value; SourceLocation location; }; struct RawThermalFlux { std::string target; int dof; double power; SourceLocation location; }; struct RawStep { SourceLocation location; std::string name_raw; std::string name_canonical; bool has_static{false}; bool static_direct{false}; std::uint32_t static_increment_count{0U}; std::optional dynamic; bool perturbation_explicit{false}; bool nlgeom_explicit{false}; StaticProcedureKind procedure_kind{ StaticProcedureKind::kLinearSmallDisplacement}; bool nlgeom{false}; std::uint32_t maximum_increments{0U}; std::array static_values{}; std::optional frequency; bool has_heat_transfer{false}; double thermal_increment{0.0}; double thermal_period{0.0}; std::vector thermal_boundaries; std::vector thermal_fluxes; std::vector boundaries; std::vector loads; std::vector surface_pressure_loads; std::size_t next_load_ordinal{0U}; }; struct MappingFailure { FailureCategory category; Diagnostic diagnostic; }; } // namespace /// @brief Builds one complete Domain candidate while retaining source identity. /// @note The candidate is committed only after all blocks and cross-references /// have been validated. class DomainMappingContext::Impl { public: explicit Impl(const ParsedInput& input) : input_{input} {} Status ParseSyntax() { ParseBlocks(); return CurrentStatus(); } Status FinalizeMaterialProperties() { if (failure_) { return CurrentStatus(); } ValidateCardinality(); if (!failure_) { FinalizeMaterials(); } if (!failure_) { FinalizePartsAndSections(); } if (!failure_) { FinalizeDensities(); } return CurrentStatus(); } Status FinalizeTopology() { if (failure_) { return CurrentStatus(); } ExpandInstances(); if (!failure_ && !definition_.shell_elements.empty()) { auto geometry = PreprocessShellGeometry(definition_.nodes, definition_.shell_elements, definition_.shell_sections); if (!geometry.HasValue()) { const auto& status = geometry.GetStatus(); failure_ = MappingFailure{status.Category().value_or(FailureCategory::kModel), status.Diagnostics().front()}; } else { definition_.shell_node_initial_frames = std::move(geometry.Value().nodal_frames); } } if (!failure_) { ExpandAssemblySets(); } return CurrentStatus(); } Status FinalizeElementSurfaces() { if (!failure_) { ResolveElementSurfaces(); } return CurrentStatus(); } Status FinalizeStepCandidate() { if (!failure_) { FinalizeStep(); } return CurrentStatus(); } Result Commit() { if (failure_) { return Result::Failure(Status::Failure( failure_->category, {std::move(failure_->diagnostic)})); } SortDiagnostics(definition_.warnings); return Domain::Create(std::move(definition_)); } DomainMappingCandidateSummary CandidateSummary() const noexcept { DomainMappingCandidateSummary summary{}; summary.part_count = parts_.size(); summary.instance_count = instances_.size(); summary.material_count = materials_.size(); 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; for (const auto& part : parts_) { summary.node_count += part.nodes.size(); summary.element_count += part.elements.size(); summary.node_set_count += part.node_sets.size(); summary.element_set_count += part.element_sets.size(); summary.beam_section_count += part.sections.size(); summary.shell_section_count += part.shell_sections.size(); } for (const auto& set : assembly_sets_) { if (set.is_node_set) { ++summary.node_set_count; } else { ++summary.element_set_count; } } return summary; } private: Status CurrentStatus() const { if (!failure_) { return Status::Ok(); } return Status::Failure(failure_->category, {failure_->diagnostic}); } const KeywordParameter* Parameter(const KeywordBlock& block, std::string_view name) const { const auto found = std::find_if(block.parameters.begin(), block.parameters.end(), [name](const KeywordParameter& candidate) { return candidate.name == name; }); return found == block.parameters.end() ? nullptr : &*found; } bool Fail(FailureCategory category, std::string code, const SourceLocation& location, std::string keyword, std::string entity_identity, std::string message) { if (!failure_) { failure_ = MappingFailure{ category, {Severity::kError, std::move(code), location, std::move(keyword), std::move(entity_identity), std::move(message)}}; } return false; } bool InputFailure(std::string code, const SourceLocation& location, std::string keyword, std::string entity_identity, std::string message) { return Fail(FailureCategory::kInput, std::move(code), location, std::move(keyword), std::move(entity_identity), std::move(message)); } bool ModelFailure(std::string code, const SourceLocation& location, std::string keyword, std::string entity_identity, std::string message) { return Fail(FailureCategory::kModel, std::move(code), location, std::move(keyword), std::move(entity_identity), std::move(message)); } bool InvalidKeywordLocation(const KeywordBlock& block, std::string message) { return InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", std::move(message)); } bool ValidateParameters( const KeywordBlock& block, const std::vector& allowed, std::string_view unsupported_code = "invalid-keyword-parameter") { std::set seen; for (const auto& candidate : block.parameters) { if (!seen.insert(candidate.name).second) { return InputFailure("duplicate-entity", block.location, block.canonical_name, candidate.name, "A keyword parameter may be declared only once."); } if (std::find(allowed.begin(), allowed.end(), candidate.name) == allowed.end()) { return InputFailure(std::string{unsupported_code}, block.location, block.canonical_name, candidate.name, "The keyword parameter is outside the approved " "subset."); } } return true; } const std::string* RequiredParameterValue(const KeywordBlock& block, std::string_view name) { const auto* found = Parameter(block, name); if (found == nullptr || !found->value || found->value->empty()) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, std::string{name}, "The keyword requires a non-empty parameter value."); return nullptr; } return &*found->value; } bool RequireNoData(const KeywordBlock& block) { if (!block.data.empty()) { return InputFailure("invalid-data-arity", block.data.front().location, block.canonical_name, "", "This keyword does not accept data rows."); } return true; } bool ParseInteger(const std::string& text, std::int64_t& value, const SourceLocation& location, const std::string& keyword, bool positive) { if (text.empty()) { return InputFailure("invalid-numeric-value", location, keyword, text, "A numeric field cannot be empty."); } char* end = nullptr; errno = 0; const long long parsed = std::strtoll(text.c_str(), &end, 10); if (errno == ERANGE || end != text.c_str() + text.size() || (positive && parsed <= 0)) { return InputFailure( "invalid-numeric-value", location, keyword, text, "The field must be a valid positive base-10 integer."); } value = static_cast(parsed); return true; } bool ParseDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword) { if (text.empty()) { return InputFailure("invalid-numeric-value", location, keyword, text, "A numeric 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 InputFailure("invalid-numeric-value", location, keyword, text, "The field must be a finite floating-point value."); } value = parsed; return true; } /// @brief Parses finite thermal decimal scalars without strtod underflow /// flags. /// @note from_chars preserves representable subnormals and rejects values /// outside binary64 range; structural scalar parsing remains unchanged. bool ParseThermalDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword) { if (text.empty() || (text.front() == '+' && (text.size() == 1U || text[1U] == '+' || text[1U] == '-'))) { return InputFailure("invalid-numeric-value", location, keyword, text, "A thermal scalar requires finite decimal syntax."); } const char* begin = text.data() + (text.front() == '+' ? 1U : 0U); const char* end = text.data() + text.size(); const auto parsed = std::from_chars(begin, end, value, std::chars_format::general); if (parsed.ec != std::errc{} || parsed.ptr != end || !std::isfinite(value)) { return InputFailure("invalid-numeric-value", location, keyword, text, "A thermal scalar must be representable and finite."); } return true; } bool ParseSurfacePressureDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword) { std::size_t cursor = 0U; if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { ++cursor; } const std::size_t integer_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } const bool has_integer_digits = cursor != integer_start; bool has_fraction_digits = false; if (cursor < text.size() && text[cursor] == '.') { ++cursor; const std::size_t fraction_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } has_fraction_digits = cursor != fraction_start; } if (!has_integer_digits && !has_fraction_digits) { return InputFailure("invalid-numeric-value", location, keyword, text, "Pressure values require C-locale decimal syntax."); } if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { ++cursor; if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { ++cursor; } const std::size_t exponent_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } if (cursor == exponent_start) { return InputFailure("invalid-numeric-value", location, keyword, text, "Pressure exponents require one or more digits."); } } if (cursor != text.size()) { return InputFailure("invalid-numeric-value", location, keyword, text, "Pressure values require C-locale decimal syntax."); } 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 InputFailure("invalid-numeric-value", location, keyword, text, "Pressure values must be finite C-locale values."); } return true; } bool ParseNonlinearStaticDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword) { if (text.empty()) { return 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 InputFailure("invalid-nonlinear-static-data", location, keyword, text, "All four nonlinear STATIC fields must be finite."); } value = parsed; return true; } bool ParseDynamicDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword) { if (text.empty()) { return InputFailure("invalid-dynamic-data", location, keyword, text, "Dynamic data fields cannot be empty."); } char* end = nullptr; errno = 0; const double parsed = std::strtod(text.c_str(), &end); if (errno == ERANGE || end != text.c_str() + text.size() || !std::isfinite(parsed)) { return InputFailure("invalid-dynamic-data", location, keyword, text, "Dynamic data fields must be finite."); } value = parsed; return true; } bool ParseModelDouble(const std::string& text, double& value, const SourceLocation& location, const std::string& keyword, const std::string& diagnostic_code, const std::string& message) { if (text.empty()) { return InputFailure("invalid-numeric-value", location, keyword, text, "A numeric field cannot be empty."); } char* end = nullptr; errno = 0; const double parsed = std::strtod(text.c_str(), &end); if (end != text.c_str() + text.size()) { return InputFailure("invalid-numeric-value", location, keyword, text, "The field must use floating-point syntax."); } if (errno == ERANGE || !std::isfinite(parsed)) { return ModelFailure(diagnostic_code, location, keyword, text, message); } value = parsed; return true; } bool ContainsName(const std::vector& values, const std::string& name) const { return std::any_of(values.begin(), values.end(), [&name](const RawPart& value) { return AsciiCaseInsensitiveEquals(value.name, name); }); } bool ContainsName(const std::vector& values, const std::string& name) const { return std::any_of(values.begin(), values.end(), [&name](const RawInstance& value) { return AsciiCaseInsensitiveEquals(value.name, name); }); } bool ContainsName(const std::vector& values, const std::string& name) const { return std::any_of(values.begin(), values.end(), [&name](const RawMaterial& value) { return AsciiCaseInsensitiveEquals(value.name, name); }); } bool ContainsSetName(const std::vector& sets, const std::string& name) const { const auto matches = [&name](const RawSet& set) { return AsciiCaseInsensitiveEquals(set.name, name); }; return std::any_of(sets.begin(), sets.end(), matches); } bool ContainsAssemblySetName(bool node_set, const std::string& name) const { return std::any_of(assembly_sets_.begin(), assembly_sets_.end(), [node_set, &name](const RawAssemblySet& set) { return set.is_node_set == node_set && AsciiCaseInsensitiveEquals(set.name, name); }); } bool ContainsSurfaceName(const std::string& name) const { return std::any_of(element_surfaces_.begin(), element_surfaces_.end(), [&name](const RawElementSurface& surface) { return AsciiCaseInsensitiveEquals(surface.name, name); }); } bool IsPressureNameToken(const std::string_view text) const noexcept { if (text.empty() || text.size() > 80U) { return false; } const auto is_initial = [](const char value) { return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || value == '_'; }; const auto is_subsequent = [is_initial](const char value) { return is_initial(value) || (value >= '0' && value <= '9') || value == '-' || value == '_'; }; return is_initial(text.front()) && std::all_of(text.begin() + 1, text.end(), is_subsequent); } void ParseBlocks() { definition_.source_path = input_.source_path; definition_.source_content_identity = input_.source_content_identity; for (std::size_t index = 0U; index < input_.blocks.size() && !failure_; ++index) { const auto& block = input_.blocks[index]; if (in_instance_) { ParseInstanceBlock(block); } else if (current_part_) { ParsePartBlock(block); } else if (in_assembly_) { ParseAssemblyBlock(block); } else if (in_step_) { ParseStepBlock(block); } else { ParseTopLevelBlock(block, index); } } if (!failure_ && (current_part_ || in_assembly_ || in_instance_ || in_step_)) { const auto location = input_.blocks.empty() ? SourceLocation{input_.source_path, 0U} : input_.blocks.back().location; InputFailure("unclosed-keyword-block", location, "", "", "A part, assembly, instance, or step block was not closed."); } } void ParseTopLevelBlock(const KeywordBlock& block, std::size_t index) { if (step_seen_) { if (block.canonical_name == "STEP") { ParseStepStart(block); } else { InvalidKeywordLocation( block, "The sole analysis step must be the final top-level block."); } return; } if (block.canonical_name != "ELASTIC" && block.canonical_name != "DENSITY" && block.canonical_name != "CONDUCTIVITY" && block.canonical_name != "SPECIFIC HEAT") { material_eligible_.reset(); } pending_section_.reset(); active_output_ = false; if (block.canonical_name == "HEADING") { if (index != 0U || heading_seen_ || !ValidateParameters(block, {})) { if (!failure_) { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", "HEADING is optional only as the first keyword."); } return; } heading_seen_ = true; for (std::size_t row = 0U; row < block.data.size(); ++row) { if (row != 0U) { definition_.heading.push_back('\n'); } for (std::size_t field = 0U; field < block.data[row].fields.size(); ++field) { if (field != 0U) { definition_.heading.push_back(','); } definition_.heading += block.data[row].fields[field]; } } return; } if (block.canonical_name == "PREPRINT") { if (!RequireNoData(block)) { return; } AddIgnoredWarning(block); return; } if (block.canonical_name == "PHYSICAL CONSTANTS") { if (!ValidateParameters(block, {"ABSOLUTE ZERO"}) || !RequireNoData(block)) return; const auto* absolute_zero = RequiredParameterValue(block, "ABSOLUTE ZERO"); if (absolute_zero == nullptr) return; double value = 0.0; if (ParseThermalDouble(*absolute_zero, value, block.location, block.canonical_name)) { if (!physical_constants_location_) physical_constants_location_ = block.location; AddIgnoredWarning(block); } return; } if (block.canonical_name == "PART") { ParsePartStart(block); return; } if (block.canonical_name == "ASSEMBLY") { ParseAssemblyStart(block); return; } if (block.canonical_name == "AMPLITUDE") { ParseAmplitude(block); return; } if (block.canonical_name == "MATERIAL") { ParseMaterial(block); return; } if (block.canonical_name == "ELASTIC") { ParseElastic(block); return; } if (block.canonical_name == "DENSITY") { if (!material_eligible_) { RejectUnknown(block); return; } ParseDensity(block); return; } if (block.canonical_name == "CONDUCTIVITY") { ParseThermalProperty(block, true); return; } if (block.canonical_name == "SPECIFIC HEAT") { ParseThermalProperty(block, false); return; } if (block.canonical_name == "INITIAL CONDITIONS") { ParseInitialConditions(block); return; } if (block.canonical_name == "BOUNDARY") { if (!assembly_seen_ || materials_.empty()) { InvalidKeywordLocation( block, "Model boundary data follows the assembly and materials."); return; } model_boundary_seen_ = true; if (model_element_family_ == ElementFamily::kThermalShell) { ParseThermalBoundary(block, thermal_model_boundaries_); } else { ParseBoundary(block, model_boundaries_); } return; } if (block.canonical_name == "STEP") { ParseStepStart(block); return; } if (block.canonical_name == "END PART" || block.canonical_name == "END ASSEMBLY" || block.canonical_name == "END INSTANCE" || block.canonical_name == "END STEP") { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", "The closing keyword has no matching open block."); return; } RejectUnknown(block); } void ParsePartStart(const KeywordBlock& block) { if (assembly_seen_ || !materials_.empty() || model_boundary_seen_) { InvalidKeywordLocation( block, "All part blocks must precede the sole assembly block."); return; } if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { return; } const auto* name = RequiredParameterValue(block, "NAME"); if (name == nullptr) { return; } if (ContainsName(parts_, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Part names are unique under case-insensitive lookup."); return; } parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}}); current_part_ = parts_.size() - 1U; part_elements_seen_ = false; part_sets_seen_ = false; part_sections_seen_ = false; beam_section_context_active_ = false; } void ParsePartBlock(const KeywordBlock& block) { if (block.canonical_name == "END PART") { if (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } const RawPart& part = parts_[*current_part_]; const bool shell_part = model_element_family_ == ElementFamily::kShell || model_element_family_ == ElementFamily::kThermalShell; if (part.nodes.empty() || part.elements.empty() || (!shell_part && model_element_family_ != ElementFamily::kSolid && part.sections.empty())) { InvalidKeywordLocation(block, "A part closes only after node, element, " "and matching section blocks."); return; } current_part_.reset(); pending_section_.reset(); beam_section_context_active_ = false; return; } if (block.canonical_name == "PART") { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", "Nested part blocks are not supported."); return; } if (block.canonical_name == "ASSEMBLY") { InputFailure("unsupported-nested-assembly", block.location, block.canonical_name, "", "An assembly cannot be nested in a part."); return; } RawPart& part = parts_[*current_part_]; if (block.canonical_name == "NODE") { beam_section_context_active_ = false; if (part_elements_seen_ || part_sets_seen_ || part_sections_seen_) { InvalidKeywordLocation( block, "NODE blocks must precede element, set, and section blocks."); return; } pending_section_.reset(); ParseNodes(block, part); } else if (block.canonical_name == "ELEMENT") { beam_section_context_active_ = false; if (part.nodes.empty() || part_sets_seen_ || part_sections_seen_) { InvalidKeywordLocation(block, "ELEMENT blocks follow at least one NODE block " "and precede sets and sections."); return; } pending_section_.reset(); ParseElements(block, part); part_elements_seen_ = !failure_; } else if (block.canonical_name == "NSET") { beam_section_context_active_ = false; if (!part_elements_seen_ || part_sections_seen_) { InvalidKeywordLocation( block, "Part sets follow element blocks and precede beam sections."); return; } pending_section_.reset(); ParseSet(block, true, part); part_sets_seen_ = !failure_; } else if (block.canonical_name == "ELSET") { beam_section_context_active_ = false; if (!part_elements_seen_ || part_sections_seen_) { InvalidKeywordLocation( block, "Part sets follow element blocks and precede beam sections."); return; } pending_section_.reset(); ParseSet(block, false, part); part_sets_seen_ = !failure_; } else if (block.canonical_name == "BEAM GENERAL SECTION") { if (!part_elements_seen_) { InvalidKeywordLocation( block, "BEAM GENERAL SECTION follows the part mesh and optional sets."); return; } if (model_element_family_ == ElementFamily::kShell || model_element_family_ == ElementFamily::kThermalShell || model_element_family_ == ElementFamily::kSolid) { InputFailure( "unsupported-mixed-element-model", block.location, block.canonical_name, "", "Beam-section semantics cannot be mixed with shell elements."); return; } ParseBeamSection(block, part); part_sections_seen_ = !failure_; beam_section_context_active_ = !failure_; } else if (block.canonical_name == "SHELL SECTION") { beam_section_context_active_ = false; if (!part_elements_seen_) { InvalidKeywordLocation( block, "SHELL SECTION follows the part mesh and optional sets."); return; } if (model_element_family_ == ElementFamily::kBeam || model_element_family_ == ElementFamily::kSolid) { InputFailure( "unsupported-mixed-element-model", block.location, block.canonical_name, "", "Shell-section semantics cannot be mixed with beam elements."); return; } ParseShellSection(block, part); part_sections_seen_ = !failure_; } else if (block.canonical_name == "SOLID SECTION") { beam_section_context_active_ = false; if (!part_elements_seen_ || model_element_family_ != ElementFamily::kSolid) { InputFailure("invalid-solid-section", block.location, block.canonical_name, "", "SOLID SECTION requires an exclusive C3D8 mesh."); return; } ParseSolidSection(block, part); part_sections_seen_ = !failure_; } else if (block.canonical_name == "SECTION POINTS") { ParseSectionPoints(block, part); } else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") { pending_section_.reset(); if (!beam_section_context_active_) { InputFailure( "invalid-keyword-location", block.location, block.canonical_name, "", "The ignored shear keyword still requires beam-section context."); return; } AddIgnoredWarning(block); } else if (block.canonical_name == "SURFACE") { InputFailure("unsupported-surface-scope", block.location, block.canonical_name, "", "Pressure surfaces are supported only at assembly scope."); } else { pending_section_.reset(); beam_section_context_active_ = false; RejectUnknown(block); } } void ParseNodes(const KeywordBlock& block, RawPart& part) { if (!ValidateParameters(block, {}) || block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "NODE requires at least one four-field data row."); } return; } for (const auto& row : block.data) { if (row.fields.size() != 4U) { InputFailure("invalid-data-arity", row.location, block.canonical_name, "", "NODE rows require label, x, y, z."); return; } RawNode node{}; node.label_text = row.fields[0]; node.location = row.location; if (!ParseInteger(row.fields[0], node.label, row.location, block.canonical_name, true)) { return; } if (std::any_of(part.nodes.begin(), part.nodes.end(), [&node](const RawNode& existing) { return existing.label == node.label; })) { InputFailure("duplicate-entity", row.location, block.canonical_name, node.label_text, "Node labels are unique within a part."); return; } for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { if (!ParseDouble(row.fields[coordinate + 1U], node.coordinates[coordinate], row.location, block.canonical_name)) { return; } } part.nodes.push_back(std::move(node)); } } void ParseElements(const KeywordBlock& block, RawPart& part) { const auto* type_parameter = Parameter(block, "TYPE"); const bool thermal_block = type_parameter && type_parameter->value && AsciiCaseInsensitiveEquals(*type_parameter->value, "DS4"); if (!ValidateParameters( block, (thermal_block || (type_parameter && type_parameter->value && AsciiCaseInsensitiveEquals( *type_parameter->value, "C3D8"))) ? std::vector{"TYPE", "ELSET"} : std::vector{"TYPE"})) { return; } const auto* type = RequiredParameterValue(block, "TYPE"); if (type == nullptr) { return; } RawElement::Type element_type{}; ElementFamily element_family{}; std::size_t expected_field_count = 0U; if (AsciiCaseInsensitiveEquals(*type, "B33")) { element_type = RawElement::Type::kB33; element_family = ElementFamily::kBeam; expected_field_count = 3U; } else if (AsciiCaseInsensitiveEquals(*type, "S4")) { element_type = RawElement::Type::kS4; element_family = ElementFamily::kShell; expected_field_count = 5U; } else if (AsciiCaseInsensitiveEquals(*type, "S4R")) { element_type = RawElement::Type::kS4r; element_family = ElementFamily::kShell; expected_field_count = 5U; } else if (AsciiCaseInsensitiveEquals(*type, "C3D8")) { element_type = RawElement::Type::kC3d8; element_family = ElementFamily::kSolid; expected_field_count = 9U; } else if (AsciiCaseInsensitiveEquals(*type, "DS4")) { element_type = RawElement::Type::kDs4; element_family = ElementFamily::kThermalShell; expected_field_count = 5U; } else { InputFailure("unsupported-element-formulation", block.location, block.canonical_name, *type, "Only B33, S4, S4R, DS4, and C3D8 belong to " "the approved " "element subsets."); return; } if (model_element_family_ && *model_element_family_ != element_family) { InputFailure( "unsupported-mixed-element-model", block.location, block.canonical_name, *type, "Beam, Shell, thermal, and Solid families cannot mix in one model."); return; } model_element_family_ = element_family; std::optional inline_set; if (Parameter(block, "ELSET")) { const auto* name = RequiredParameterValue(block, "ELSET"); if (name == nullptr) return; if (ContainsSetName(part.element_sets, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Element-set names are unique within their part."); return; } inline_set = RawSet{*name, {}, block.location}; } if (block.data.empty()) { InputFailure(element_family == ElementFamily::kSolid ? "invalid-solid-connectivity" : (element_family == ElementFamily::kShell ? "invalid-shell-connectivity" : "invalid-data-arity"), block.location, block.canonical_name, "", "ELEMENT requires at least one row with the approved " "connectivity arity."); return; } for (const auto& row : block.data) { if (row.fields.size() != expected_field_count) { InputFailure( element_family == ElementFamily::kSolid ? "invalid-solid-connectivity" : (element_family == ElementFamily::kShell ? "invalid-shell-connectivity" : "invalid-data-arity"), row.location, block.canonical_name, "", element_family == ElementFamily::kShell ? "S4 and S4R rows require a label and exactly four nodes." : "B33 rows require label, node 1, node 2."); return; } RawElement element{}; element.label_text = row.fields[0]; element.type = element_type; element.location = row.location; if (!ParseInteger(row.fields[0], element.label, row.location, block.canonical_name, true)) { return; } element.node_labels.reserve(expected_field_count - 1U); for (std::size_t field = 1U; field < expected_field_count; ++field) { std::int64_t node_label = 0; if (!ParseInteger(row.fields[field], node_label, row.location, block.canonical_name, true)) { return; } element.node_labels.push_back(node_label); } if (element_family != ElementFamily::kBeam) { const std::set distinct_nodes{element.node_labels.begin(), element.node_labels.end()}; if (distinct_nodes.size() != element.node_labels.size()) { InputFailure(element_family == ElementFamily::kSolid ? "invalid-solid-connectivity" : "invalid-shell-connectivity", row.location, block.canonical_name, element.label_text, "Connectivity requires the specified number of distinct " "source nodes."); return; } } if (std::any_of(part.elements.begin(), part.elements.end(), [&element](const RawElement& existing) { return existing.label == element.label; })) { InputFailure("duplicate-entity", row.location, block.canonical_name, element.label_text, "Element labels are unique within a part."); return; } if (inline_set) inline_set->members.push_back(element.label); part.elements.push_back(std::move(element)); } // Inline membership follows the block's source rows and uses the same // part-set namespace and section resolution as an explicit ELSET. if (inline_set) part.element_sets.push_back(std::move(*inline_set)); } /// @brief Parses material-only Solid assignment without thickness defaults. void ParseSolidSection(const KeywordBlock& block, RawPart& part) { if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) return; const auto* elset = RequiredParameterValue(block, "ELSET"); const auto* material = RequiredParameterValue(block, "MATERIAL"); if (!elset || !material) return; if (block.data.size() > 1U || (!block.data.empty() && std::any_of(block.data[0].fields.begin(), block.data[0].fields.end(), [](const auto& field) { return !field.empty(); }))) { InputFailure("invalid-solid-section", block.location, block.canonical_name, *elset, "SOLID SECTION permits omitted or blank data only."); return; } part.solid_sections.push_back({*elset, *material, block.location}); } /// @brief Validates and owns one model-level supported load amplitude. void 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) { InputFailure("unsupported-load-amplitude", block.location, block.canonical_name, "", "Amplitude is supported only in Solid dynamic models."); return; } if (!ValidateParameters(block, {"NAME", "DEFINITION", "TIME", "VALUE"})) return; const auto* name = RequiredParameterValue(block, "NAME"); if (!name) return; for (const auto& option : std::array, 3>{ {{"DEFINITION", "TABULAR"}, {"TIME", "STEP TIME"}, {"VALUE", "RELATIVE"}}}) { const auto* parameter = Parameter(block, option.first); if (parameter && (!parameter->value || !AsciiCaseInsensitiveEquals(*parameter->value, option.second))) { InputFailure("unsupported-amplitude-option", block.location, block.canonical_name, option.first, "Only TABULAR STEP TIME RELATIVE amplitude is supported."); return; } } const std::string canonical = CanonicalizeAscii(*name); if (std::any_of(definition_.amplitudes.begin(), definition_.amplitudes.end(), [&canonical](const auto& a) { return a.name_canonical == canonical; })) { 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) { 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 (!ParseDouble(row.fields[field], time, row.location, block.canonical_name) || !ParseDouble(row.fields[field + 1U], value, row.location, block.canonical_name)) return; if (time < 0.0 || (!amplitude.points.empty() && !(time > amplitude.points.back()[0]))) { 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()) { 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 ParseShellSection(const KeywordBlock& block, RawPart& part) { for (const auto& candidate : block.parameters) { if (candidate.name != "ELSET" && candidate.name != "MATERIAL") { InputFailure("unsupported-shell-section-option", block.location, block.canonical_name, candidate.name, "The shell-section option is outside the centered " "single-layer subset."); return; } } if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) { return; } const auto* element_set = RequiredParameterValue(block, "ELSET"); const auto* material = RequiredParameterValue(block, "MATERIAL"); if (element_set == nullptr || material == nullptr) { return; } if (block.data.size() != 1U || block.data[0].fields.empty() || block.data[0].fields.size() > 2U) { InputFailure("unsupported-shell-section-option", block.location, block.canonical_name, *element_set, "SHELL SECTION requires one thickness row with one optional " "integration-point field."); return; } double thickness = 0.0; const bool parsed_thickness = model_element_family_ == ElementFamily::kThermalShell ? ParseThermalDouble(block.data[0].fields[0], thickness, block.data[0].location, block.canonical_name) : ParseModelDouble(block.data[0].fields[0], thickness, block.data[0].location, block.canonical_name, "invalid-shell-thickness", "Shell thickness must be finite and positive."); if (!parsed_thickness) { return; } if (!(thickness > 0.0)) { ModelFailure("invalid-shell-thickness", block.data[0].location, block.canonical_name, *element_set, "Shell thickness must be finite and positive."); return; } if (block.data[0].fields.size() == 2U) { const auto integration_points = ParsePositiveSourceLabel(block.data[0].fields[1]); if (!integration_points.HasValue()) { InputFailure("unsupported-shell-section-option", block.data[0].location, block.canonical_name, block.data[0].fields[1], "The optional shell integration-point field must be a " "positive integer."); return; } if (model_element_family_ == ElementFamily::kThermalShell && integration_points.Value() != 5) { InputFailure( "unsupported-shell-section-option", block.data[0].location, block.canonical_name, block.data[0].fields[1], "Thermal DS4 sections require exactly five integration points."); return; } } part.shell_sections.push_back( {*element_set, *material, thickness, block.location}); } bool ParseSetMembers(const KeywordBlock& block, bool generate, std::vector& members) { if (block.data.empty()) { return InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "A set requires at least one member row."); } for (const auto& data : block.data) { auto fields = WithoutTrailingEmpty(data.fields); if (generate) { if (fields.size() != 3U) { return InputFailure("invalid-data-arity", data.location, block.canonical_name, "", "GENERATE rows require first, last, increment."); } std::int64_t first = 0; std::int64_t last = 0; std::int64_t increment = 0; if (!ParseInteger(fields[0], first, data.location, block.canonical_name, true) || !ParseInteger(fields[1], last, data.location, block.canonical_name, true)) { return false; } if (!ParseInteger(fields[2], increment, data.location, block.canonical_name, false) || increment <= 0 || first > last || (last - first) % increment != 0) { return InputFailure("invalid-set-range", data.location, block.canonical_name, "", "GENERATE requires an inclusive range reached by " "a positive increment."); } for (std::int64_t label = first; label <= last;) { members.push_back(label); if (label > last - increment) { break; } label += increment; } } else { if (fields.empty() || std::any_of(fields.begin(), fields.end(), [](const std::string& field) { return field.empty(); })) { return InputFailure( "invalid-data-arity", data.location, block.canonical_name, "", "Explicit set rows require non-empty member labels."); } for (const auto& field : fields) { std::int64_t label = 0; if (!ParseInteger(field, label, data.location, block.canonical_name, true)) { return false; } members.push_back(label); } } } std::set unique; if (model_element_family_ == ElementFamily::kThermalShell) { // Thermal sets expand each source member once, in first-seen order. members.erase(std::remove_if(members.begin(), members.end(), [&unique](const auto member) { return !unique.insert(member).second; }), members.end()); return true; } for (const auto member : members) { if (!unique.insert(member).second) { return InputFailure("duplicate-entity", block.location, block.canonical_name, std::to_string(member), "A set cannot repeat the same source member."); } } return true; } void ParseSet(const KeywordBlock& block, bool node_set, RawPart& part) { const std::string_view name_parameter = node_set ? "NSET" : "ELSET"; if (!ValidateParameters(block, {name_parameter, "GENERATE"})) { return; } const auto* name = RequiredParameterValue(block, name_parameter); if (name == nullptr) { return; } const auto* generate_parameter = Parameter(block, "GENERATE"); if (generate_parameter != nullptr && generate_parameter->value) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, "GENERATE", "GENERATE is a valueless flag."); return; } const auto& sets = node_set ? part.node_sets : part.element_sets; if (ContainsSetName(sets, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Set names are unique within their node-set or element-set " "namespace."); return; } RawSet set{*name, {}, block.location}; if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { return; } if (node_set) { part.node_sets.push_back(std::move(set)); } else { part.element_sets.push_back(std::move(set)); } } void ParseBeamSection(const KeywordBlock& block, RawPart& part) { pending_section_.reset(); if (!ValidateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) { return; } const auto* element_set = RequiredParameterValue(block, "ELSET"); const auto* material = RequiredParameterValue(block, "MATERIAL"); const auto* section = RequiredParameterValue(block, "SECTION"); if (element_set == nullptr || material == nullptr || section == nullptr) { return; } if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) { InputFailure("unsupported-section-formulation", block.location, block.canonical_name, *section, "Only SECTION=GENERAL belongs to the approved subset."); return; } if (std::any_of(part.sections.begin(), part.sections.end(), [element_set](const RawSection& existing) { return AsciiCaseInsensitiveEquals( existing.element_set_name, *element_set); })) { InputFailure("duplicate-entity", block.location, block.canonical_name, *element_set, "An element set can receive only one beam section."); return; } if (block.data.size() != 2U || block.data[0].fields.size() != 5U || block.data[1].fields.size() != 3U) { InputFailure("invalid-data-arity", block.location, block.canonical_name, *element_set, "A general section requires one five-field property row and " "one three-field axis row."); return; } RawSection raw{*element_set, *material, {}, {}, {}, block.location}; for (std::size_t property = 0U; property < 5U; ++property) { if (!ParseModelDouble(block.data[0].fields[property], raw.properties[property], block.data[0].location, block.canonical_name, "invalid-beam-property", "Beam section properties must be finite.")) { return; } } for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) { if (!ParseModelDouble(block.data[1].fields[coordinate], raw.first_axis[coordinate], block.data[1].location, block.canonical_name, "invalid-beam-guide-vector", "The beam guide vector must be finite.")) { return; } } part.sections.push_back(std::move(raw)); pending_section_ = part.sections.size() - 1U; } void ParseSectionPoints(const KeywordBlock& block, RawPart& part) { if (!pending_section_ || !ValidateParameters(block, {}) || block.data.empty()) { if (!failure_) { InputFailure( "invalid-keyword-location", block.location, block.canonical_name, "", "SECTION POINTS must immediately follow a general section."); } return; } RawSection& section = part.sections[*pending_section_]; for (const auto& row : block.data) { if (row.fields.size() != 2U) { InputFailure("invalid-data-arity", row.location, block.canonical_name, section.element_set_name, "Section-point rows require x1 and x2."); return; } std::array point{}; if (!ParseDouble(row.fields[0], point[0], row.location, block.canonical_name) || !ParseDouble(row.fields[1], point[1], row.location, block.canonical_name)) { return; } if (std::find(section.section_points.begin(), section.section_points.end(), point) != section.section_points.end()) { InputFailure("duplicate-entity", row.location, block.canonical_name, section.element_set_name, "Section points must be unique within a section."); return; } section.section_points.push_back(point); } pending_section_.reset(); } void ParseAssemblyStart(const KeywordBlock& block) { if (assembly_seen_) { InputFailure("unsupported-nested-assembly", block.location, block.canonical_name, "", "V0 accepts exactly one non-nested assembly."); return; } if (parts_.empty() || !materials_.empty() || model_boundary_seen_) { InvalidKeywordLocation( block, "The sole assembly follows all part blocks and precedes model data."); return; } if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { return; } if (RequiredParameterValue(block, "NAME") == nullptr) { return; } assembly_seen_ = true; in_assembly_ = true; assembly_set_seen_ = false; } void ParseAssemblyBlock(const KeywordBlock& block) { if (block.canonical_name == "END ASSEMBLY") { if (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } if (instances_.empty()) { InvalidKeywordLocation( block, "The assembly requires at least one identity instance."); return; } in_assembly_ = false; return; } if (block.canonical_name == "ASSEMBLY") { InputFailure("unsupported-nested-assembly", block.location, block.canonical_name, "", "Nested or duplicate assembly blocks are unsupported."); } else if (block.canonical_name == "INSTANCE") { if (assembly_set_seen_) { InvalidKeywordLocation( block, "All identity instances must precede assembly-level sets."); return; } ParseInstanceStart(block); } else if (block.canonical_name == "NSET") { if (instances_.empty()) { InvalidKeywordLocation( block, "Assembly sets follow at least one identity instance."); return; } assembly_set_seen_ = true; ParseAssemblySet(block, true); } else if (block.canonical_name == "ELSET") { if (instances_.empty()) { InvalidKeywordLocation( block, "Assembly sets follow at least one identity instance."); return; } assembly_set_seen_ = true; ParseAssemblySet(block, false); } else if (block.canonical_name == "SURFACE") { if (instances_.empty()) { InvalidKeywordLocation( block, "Assembly surfaces follow at least one identity instance."); return; } ParseElementSurface(block); } else { RejectUnknown(block); } } void ParseInstanceStart(const KeywordBlock& block) { if (Parameter(block, "DEPENDENT") != nullptr || Parameter(block, "INDEPENDENT") != nullptr) { InputFailure( "unsupported-instance-mesh-semantics", block.location, block.canonical_name, "", "Dependent and independent instance mesh semantics are unsupported."); return; } if (!ValidateParameters(block, {"NAME", "PART"})) { return; } const auto* name = RequiredParameterValue(block, "NAME"); const auto* part = RequiredParameterValue(block, "PART"); if (name == nullptr || part == nullptr) { return; } if (!block.data.empty()) { InputFailure("unsupported-instance-transform", block.data.front().location, block.canonical_name, *name, "Instance translation or rotation data is unsupported."); return; } if (ContainsName(instances_, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Instance names are globally unique."); return; } instances_.push_back({*name, *part, block.location}); in_instance_ = true; } void ParseInstanceBlock(const KeywordBlock& block) { if (block.canonical_name == "END INSTANCE") { if (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } in_instance_ = false; return; } if (block.canonical_name == "ASSEMBLY") { InputFailure("unsupported-nested-assembly", block.location, block.canonical_name, "", "An assembly cannot be nested in an instance."); return; } if (block.canonical_name == "SURFACE") { InputFailure("unsupported-surface-scope", block.location, block.canonical_name, instances_.back().name, "Pressure surfaces are unsupported inside an instance."); return; } InputFailure("unsupported-instance-mesh-semantics", block.location, block.canonical_name, instances_.back().name, "Instance-local mesh definitions are unsupported."); } void ParseAssemblySet(const KeywordBlock& block, bool node_set) { const std::string_view name_parameter = node_set ? "NSET" : "ELSET"; const std::vector allowed_parameters = node_set ? std::vector{"NSET", "INSTANCE", "GENERATE"} : std::vector{"ELSET", "INSTANCE", "GENERATE", "INTERNAL"}; if (!ValidateParameters(block, allowed_parameters)) { return; } const auto* name = RequiredParameterValue(block, name_parameter); if (name == nullptr) { return; } const auto* instance_parameter = Parameter(block, "INSTANCE"); if (instance_parameter == nullptr || !instance_parameter->value || instance_parameter->value->empty()) { InputFailure( node_set ? "invalid-keyword-parameter" : "implicit-instance-elset", block.location, block.canonical_name, "INSTANCE", "Assembly element sets require an explicit INSTANCE."); return; } const std::string& instance = *instance_parameter->value; const auto* generate_parameter = Parameter(block, "GENERATE"); if (generate_parameter != nullptr && generate_parameter->value) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, "GENERATE", "GENERATE is a valueless flag."); return; } const auto* internal_parameter = Parameter(block, "INTERNAL"); if (internal_parameter != nullptr && internal_parameter->value) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, "INTERNAL", "INTERNAL is a valueless result-membership flag."); return; } if (ContainsAssemblySetName(node_set, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Assembly set names are unique within their node-set or " "element-set namespace."); return; } if (!node_set && generate_parameter == nullptr) { for (const auto& row : block.data) { for (const auto& field : WithoutTrailingEmpty(row.fields)) { if (field.empty()) { continue; } const bool numeric = !field.empty() && std::all_of(field.begin(), field.end(), [](const char value) { return value >= '0' && value <= '9'; }); if (!numeric) { const std::string code = AsciiCaseInsensitiveEquals(field, *name) ? "cyclic-elset" : "nested-elset"; InputFailure(code, row.location, block.canonical_name, field, "Assembly ELSET members must be direct labels."); return; } } } } RawAssemblySet set{node_set, *name, instance, {}, block.location}; if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) { return; } assembly_sets_.push_back(std::move(set)); } void ParseElementSurface(const KeywordBlock& block) { if (!ValidateParameters(block, {"TYPE", "NAME"}, "unsupported-parameter")) { return; } const auto* type = RequiredParameterValue(block, "TYPE"); const auto* name = RequiredParameterValue(block, "NAME"); if (type == nullptr || name == nullptr) { return; } if (!IsPressureNameToken(*name)) { InputFailure("invalid-surface-name", block.location, block.canonical_name, *name, "Surface names must use the approved ASCII name syntax."); return; } if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) { InputFailure("unsupported-surface-type", block.location, block.canonical_name, *type, "Only TYPE=ELEMENT belongs to the approved surface subset."); return; } if (ContainsSurfaceName(*name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Surface names are unique in the assembly namespace."); return; } if (block.data.empty()) { InputFailure("empty-surface", block.location, block.canonical_name, *name, "An element surface requires at least one member."); return; } RawElementSurface surface{ *name, block.location, element_surfaces_.size(), {}}; for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { const auto& row = block.data[ordinal]; if (row.fields.size() != 2U || row.fields[0].empty() || row.fields[1].empty()) { InputFailure("invalid-data-arity", row.location, block.canonical_name, *name, "Surface rows require one member and SPOS or SNEG."); return; } ShellSurfaceSide side = ShellSurfaceSide::kSpos; if (AsciiCaseInsensitiveEquals(row.fields[1], "SPOS")) { side = ShellSurfaceSide::kSpos; } else if (AsciiCaseInsensitiveEquals(row.fields[1], "SNEG")) { side = ShellSurfaceSide::kSneg; } else { InputFailure("unsupported-surface-side", row.location, block.canonical_name, row.fields[1], "Surface sides must be SPOS or SNEG."); return; } surface.members.push_back({row.fields[0], side, row.location, ordinal}); } element_surfaces_.push_back(std::move(surface)); if (model_element_family_ == ElementFamily::kThermalShell) AddIgnoredWarning(block); } void ParseMaterial(const KeywordBlock& block) { if (!assembly_seen_ || model_boundary_seen_) { InvalidKeywordLocation(block, "Material definitions follow the assembly and " "precede model boundaries."); return; } if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) { return; } const auto* name = RequiredParameterValue(block, "NAME"); if (name == nullptr) { return; } if (ContainsName(materials_, *name)) { InputFailure("duplicate-entity", block.location, block.canonical_name, *name, "Material names are globally unique."); return; } materials_.push_back({*name, 0.0, 0.0, false, std::nullopt, std::nullopt, std::nullopt, block.location, {}, {}}); material_eligible_ = materials_.size() - 1U; } void ParseElastic(const KeywordBlock& block) { if (model_element_family_ == ElementFamily::kThermalShell) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "ELASTIC is outside the thermal material subset."); return; } if (!material_eligible_) { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", "ELASTIC must immediately follow MATERIAL."); return; } if (!ValidateParameters(block, {}) || block.data.size() != 1U || block.data[0].fields.size() != 2U) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, materials_[*material_eligible_].name, "ELASTIC requires exactly one E, nu row."); } return; } RawMaterial& material = materials_[*material_eligible_]; if (material.has_elastic) { InputFailure("duplicate-entity", block.location, block.canonical_name, material.name, "A material accepts one ELASTIC definition."); return; } if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus, block.data[0].location, block.canonical_name, model_element_family_ == ElementFamily::kSolid ? "invalid-solid-material" : model_element_family_ == ElementFamily::kShell ? "invalid-shell-material" : "invalid-beam-property", "Elastic material values must be finite.") || !ParseModelDouble(block.data[0].fields[1], material.poisson_ratio, block.data[0].location, block.canonical_name, model_element_family_ == ElementFamily::kSolid ? "invalid-solid-material" : model_element_family_ == ElementFamily::kShell ? "invalid-shell-material" : "invalid-beam-property", "Elastic material values must be finite.")) { return; } material.has_elastic = true; material.elastic_location = block.data[0].location; } bool ParseDensityValue(const std::string& text, double& value, const SourceLocation& location) { const std::string canonical = CanonicalizeAscii(text); if (canonical == "NAN" || canonical == "+NAN" || canonical == "-NAN" || canonical == "INF" || canonical == "+INF" || canonical == "-INF" || canonical == "INFINITY" || canonical == "+INFINITY" || canonical == "-INFINITY") { return ModelFailure( "invalid-material-density", location, "DENSITY", text, "Density must convert to a finite value greater than zero."); } std::size_t cursor = 0U; if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { ++cursor; } const std::size_t integer_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } const bool has_integer_digits = cursor != integer_start; bool has_fraction_digits = false; if (cursor < text.size() && text[cursor] == '.') { ++cursor; const std::size_t fraction_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } has_fraction_digits = cursor != fraction_start; } if (!has_integer_digits && !has_fraction_digits) { return InputFailure("invalid-density-data", location, "DENSITY", text, "Density requires C-locale decimal syntax."); } if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) { ++cursor; if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) { ++cursor; } const std::size_t exponent_start = cursor; while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') { ++cursor; } if (cursor == exponent_start) { return InputFailure("invalid-density-data", location, "DENSITY", text, "Density exponents require one or more digits."); } } if (cursor != text.size()) { return InputFailure("invalid-density-data", location, "DENSITY", text, "Density requires C-locale decimal syntax."); } const char* begin = text.data(); if (*begin == '+') { ++begin; } double parsed = 0.0; const auto conversion = std::from_chars(begin, text.data() + text.size(), parsed, std::chars_format::general); if (conversion.ec != std::errc{} || conversion.ptr != text.data() + text.size() || !std::isfinite(parsed) || !(parsed > 0.0)) { return ModelFailure( "invalid-material-density", location, "DENSITY", text, "Density must convert to a finite value greater than zero."); } value = parsed; return true; } void ParseDensity(const KeywordBlock& block) { if (!material_eligible_) { InputFailure("invalid-density-data", block.location, block.canonical_name, "", "DENSITY must be declared inside one MATERIAL."); return; } if (!ValidateParameters(block, {}, "unsupported-density-option")) { return; } RawMaterial& material = materials_[*material_eligible_]; if (material.density) { InputFailure("duplicate-density", block.location, block.canonical_name, material.name, "A material accepts one DENSITY definition."); return; } if (block.data.size() != 1U || (block.data[0].fields.size() != 1U && (block.data[0].fields.size() != 2U || !block.data[0].fields[1].empty())) || block.data[0].fields[0].empty()) { InputFailure("invalid-density-data", block.location, block.canonical_name, material.name, "DENSITY requires exactly rho or rho,."); return; } double rho = 0.0; if (!ParseDensityValue(block.data[0].fields[0], rho, block.data[0].location)) { return; } material.density = rho; material.density_location = block.data[0].location; } void ParseThermalProperty(const KeywordBlock& block, bool conductivity) { if (model_element_family_ != ElementFamily::kThermalShell || !material_eligible_ || !ValidateParameters(block, {})) { if (!failure_) { InputFailure("invalid-keyword-location", block.location, block.canonical_name, "", "Thermal properties belong to a DS4 material."); } return; } RawMaterial& material = materials_[*material_eligible_]; std::optional& destination = conductivity ? material.conductivity : material.specific_heat; if (destination || block.data.size() != 1U || (block.data[0].fields.size() != 1U && (block.data[0].fields.size() != 2U || !block.data[0].fields[1].empty()))) { InputFailure("invalid-data-arity", block.location, block.canonical_name, material.name, "Thermal property requires one constant row."); return; } double value = 0.0; if (!ParseThermalDouble(block.data[0].fields[0], value, block.data[0].location, block.canonical_name) || !(value > 0.0)) { if (!failure_) { ModelFailure("invalid-thermal-material", block.data[0].location, block.canonical_name, material.name, "Thermal material values must be finite and positive."); } return; } destination = value; } void ParseInitialConditions(const KeywordBlock& block) { if (model_element_family_ != ElementFamily::kThermalShell || !ValidateParameters(block, {"TYPE"}) || block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "Thermal initial conditions require data rows."); } return; } const auto* type = RequiredParameterValue(block, "TYPE"); if (type == nullptr || !AsciiCaseInsensitiveEquals(*type, "TEMPERATURE")) { if (!failure_) 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()) { InputFailure("invalid-data-arity", row.location, block.canonical_name, "", "Initial temperatures require target, value."); return; } double value = 0.0; if (!ParseThermalDouble(row.fields[1], value, row.location, block.canonical_name)) return; thermal_initial_conditions_.push_back( {row.fields[0], value, row.location}); } } void ParseThermalBoundary(const KeywordBlock& block, std::vector& destination) { if (!ValidateParameters(block, {}) || block.data.empty()) { if (!failure_) 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()) { 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 (!ParseInteger(row.fields[1], first, row.location, block.canonical_name, true) || !ParseInteger(row.fields[2], last, row.location, block.canonical_name, true) || first != last || (first != 11 && first != 15)) { 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 && !ParseThermalDouble(row.fields[3], value, row.location, block.canonical_name)) return; destination.push_back( {row.fields[0], static_cast(first), value, row.location}); } } void 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 (!ValidateParameters(block, {}, dynamic_input ? "unsupported-dynamic-boundary" : "invalid-keyword-parameter") || block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "BOUNDARY requires one or more data rows."); } return; } for (const auto& row : block.data) { if (row.fields.size() != 3U && row.fields.size() != 4U) { 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()) { 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 (!ParseInteger(row.fields[1], first, row.location, block.canonical_name, true) || !ParseInteger(row.fields[2], last, row.location, block.canonical_name, true)) { return; } if (first < 1 || first > 6 || last < first || last > 6) { 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()) && !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 ParseCload(const KeywordBlock& block, std::vector& loads) { if (step_.dynamic && model_element_family_ != ElementFamily::kSolid && !block.parameters.empty()) { 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 (!ValidateParameters( block, solid_dynamic ? std::vector{"AMPLITUDE"} : std::vector{}) || block.data.empty()) { if (!failure_) { 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 (Parameter(block, "AMPLITUDE")) { const auto* name = 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) { 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()) { 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 (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name, true) || !ParseDouble(row.fields[2], magnitude, row.location, block.canonical_name)) { return; } if (dof < 1 || dof > 6) { 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) { 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 ParseDsload(const KeywordBlock& block) { if (!ValidateParameters(block, {}, "unsupported-dsload-parameter") || block.data.empty()) { if (!failure_) { InputFailure("invalid-data-arity", block.location, block.canonical_name, "", "DSLOAD requires one or more rows."); } return; } for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) { const auto& row = block.data[ordinal]; if (row.fields.size() != 3U || row.fields[0].empty() || row.fields[1].empty() || row.fields[2].empty()) { InputFailure("invalid-data-arity", row.location, block.canonical_name, "", "DSLOAD rows require surface, P, magnitude."); return; } if (!IsPressureNameToken(row.fields[0])) { 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")) { InputFailure("unsupported-load-type", row.location, block.canonical_name, row.fields[1], "Only uniform surface pressure load type P is supported."); return; } double magnitude = 0.0; if (!ParseSurfacePressureDouble(row.fields[2], magnitude, row.location, block.canonical_name)) { return; } step_.surface_pressure_loads.push_back( {row.fields[0], magnitude, row.fields[2], block.location, row.location, ordinal, step_.next_load_ordinal++}); } } void ParseStepStart(const KeywordBlock& block) { if (step_seen_) { if (step_.frequency || step_.dynamic) { InputFailure( "unsupported-analysis-procedure", block.location, block.canonical_name, "", "The frequency subset accepts exactly one procedure step."); } else { const auto* nlgeom = Parameter(block, "NLGEOM"); const bool nonlinear_second_step = nlgeom != nullptr && nlgeom->value && AsciiCaseInsensitiveEquals(*nlgeom->value, "YES"); if (step_.nlgeom || nonlinear_second_step) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, "", "The nonlinear static subset accepts exactly one " "procedure step."); } else { InputFailure("unsupported-multiple-step", block.location, block.canonical_name, "", "V0 accepts exactly one analysis step."); } } return; } if (!assembly_seen_ || materials_.empty()) { InvalidKeywordLocation(block, "The sole step follows the complete assembly and " "material definitions."); return; } if (!ValidateParameters(block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) || !RequireNoData(block)) { return; } StaticProcedureKind procedure_kind = StaticProcedureKind::kLinearSmallDisplacement; bool nlgeom = false; const auto* nlgeom_parameter = Parameter(block, "NLGEOM"); if (nlgeom_parameter != nullptr) { if (!nlgeom_parameter->value || nlgeom_parameter->value->empty()) { 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) { 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")) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, *nlgeom_parameter->value, "NLGEOM must select YES or NO."); return; } } const auto* perturbation = Parameter(block, "PERTURBATION"); if (perturbation != nullptr && perturbation->value) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, "PERTURBATION", "PERTURBATION is accepted only as a value-less flag."); return; } const auto* name = Parameter(block, "NAME"); if (name != nullptr && (!name->value || name->value->empty())) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, "NAME", "STEP NAME must not be empty when it is present."); return; } const auto* increment_limit = Parameter(block, "INC"); std::uint32_t maximum_increments = 100U; if (increment_limit != nullptr) { if (!increment_limit->value || increment_limit->value->empty()) { InputFailure("invalid-dynamic-data", block.location, block.canonical_name, "INC", "INC requires a positive uint32 value."); return; } std::int64_t parsed_limit = 0; if (!ParseInteger(*increment_limit->value, parsed_limit, block.location, block.canonical_name, true) || parsed_limit > static_cast( std::numeric_limits::max())) { if (!failure_) { 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 = CanonicalizeAscii(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 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)) { 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")) { 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 (!ValidateParameters(block, {}) || !RequireNoData(block)) { return; } if (!step_.has_static && !step_.frequency && !step_.dynamic && !step_.has_heat_transfer) { InvalidKeywordLocation( block, "The sole step requires one supported procedure keyword."); return; } in_step_ = false; return; } if (block.canonical_name == "STEP") { 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_) { 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_) { 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_) { 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_) { 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_) { InvalidKeywordLocation( block, "Step boundaries follow STATIC and precede loads " "and no-op requests."); return; } if (step_.dynamic && !block.parameters.empty()) { InputFailure("unsupported-dynamic-boundary", block.location, block.canonical_name, "", "Dynamic BOUNDARY does not support keyword parameters."); return; } if (step_.has_heat_transfer) { ParseThermalBoundary(block, step_.thermal_boundaries); } else { ParseBoundary(block, step_.boundaries); } if (!failure_ && step_.frequency) { ValidateEigenBoundaries(step_.boundaries); } if (!failure_ && step_.dynamic) { ValidateDynamicBoundaries(step_.boundaries); } } else if (block.canonical_name == "CLOAD") { active_output_ = false; if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) { InvalidKeywordLocation( block, "CLOAD follows STATIC and boundaries and precedes no-op requests."); return; } ParseCload(block, step_.loads); if (!failure_) { step_.next_load_ordinal += block.data.size(); } step_load_seen_ = !failure_; } else if (block.canonical_name == "CFLUX") { active_output_ = false; if (!step_.has_heat_transfer || step_no_op_seen_) { InvalidKeywordLocation(block, "CFLUX follows HEAT TRANSFER and boundaries."); return; } ParseCflux(block); step_load_seen_ = !failure_; } else if (block.canonical_name == "DSLOAD") { active_output_ = false; if (!step_.has_static || step_.dynamic || step_no_op_seen_) { InvalidKeywordLocation(block, "DSLOAD follows STATIC and boundaries and " "precedes no-op requests."); return; } ParseDsload(block); step_load_seen_ = !failure_; } else if (block.canonical_name == "RESTART") { active_output_ = false; if (!step_.has_static && !step_.frequency && !step_.dynamic && !step_.has_heat_transfer) { 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 = Parameter(block, "WRITE"); const auto* frequency = Parameter(block, "FREQUENCY"); if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) || write == nullptr || write->value || frequency == nullptr || !frequency->value || *frequency->value != "0") { if (!failure_) { InputFailure( "unsupported-keyword", block.location, block.canonical_name, "", "Supported RESTART no-op requires WRITE, FREQUENCY=0."); } return; } } if (RequireNoData(block)) { AddIgnoredWarning(block); } } else if (block.canonical_name == "OUTPUT") { if (!step_.has_static && !step_.frequency && !step_.dynamic && !step_.has_heat_transfer) { 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) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, "", "The nonlinear step supports only the STATIC procedure."); return; } RejectUnknown(block); } } bool 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 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 InputFailure( "invalid-frequency-data", location, "FREQUENCY", text, "The requested mode count must be a positive int64 value."); } return true; } void ParseFrequency(const KeywordBlock& block) { if (step_.nlgeom || (step_.perturbation_explicit && step_.has_static) || !ValidateParameters( block, {"EIGENSOLVER", "SIM", "ACOUSTIC COUPLING", "NORMALIZATION"}, "unsupported-frequency-option")) { if (!failure_) { 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) { 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()) { 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 = Parameter(block, "EIGENSOLVER"); if (eigensolver != nullptr) { if (!eigensolver->value || !AsciiCaseInsensitiveEquals(*eigensolver->value, "LANCZOS")) { InputFailure("unsupported-frequency-option", block.location, block.canonical_name, "EIGENSOLVER", "Only EIGENSOLVER=LANCZOS is accepted."); return; } frequency.eigensolver_explicit = true; } const auto* sim = Parameter(block, "SIM"); if (sim != nullptr) { if (sim->value && !AsciiCaseInsensitiveEquals(*sim->value, "YES")) { 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 = Parameter(block, "ACOUSTIC COUPLING"); if (acoustic != nullptr) { if (!acoustic->value || !AsciiCaseInsensitiveEquals(*acoustic->value, "ON")) { 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 = Parameter(block, "NORMALIZATION"); if (normalization != nullptr) { if (!normalization->value || !AsciiCaseInsensitiveEquals(*normalization->value, "MASS")) { 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 ValidateEigenBoundaries( const std::vector& boundaries) { for (const auto& boundary : boundaries) { if (boundary.value != 0.0) { InputFailure("unsupported-eigen-boundary", boundary.location, "BOUNDARY", boundary.target, "Frequency boundaries must prescribe exact zero."); return; } } } void ValidateDynamicBoundaries( const std::vector& boundaries) { for (const auto& boundary : boundaries) { if (boundary.value != 0.0) { InputFailure("unsupported-dynamic-boundary", boundary.location, "BOUNDARY", boundary.target, "Dynamic boundaries must be homogeneous and constant."); return; } } } void ParseDynamic(const KeywordBlock& block) { if (step_.perturbation_explicit || (model_element_family_ != ElementFamily::kSolid && (!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) || !ValidateParameters(block, {"DIRECT"}, "unsupported-dynamic-parameter")) { if (!failure_) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, "", "DYNAMIC requires nonlinear Shell or linear/nonlinear " "Solid geometry."); } return; } if (model_element_family_ == ElementFamily::kSolid && block.data.size() == 1U && block.data[0].fields.size() > 3U) { 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 = Parameter(block, "DIRECT"); if (direct == nullptr || direct->value || block.data.size() != 1U || block.data[0].fields.size() < 2U || std::any_of(block.data[0].fields.begin() + 2, block.data[0].fields.end(), [](const auto& field) { return !field.empty(); })) { InputFailure( "invalid-dynamic-data", block.location, block.canonical_name, "", "DYNAMIC,DIRECT requires one row with increment and time period."); return; } DynamicStepDefinition dynamic{step_.name_raw, step_.name_canonical, step_.location, block.location, block.data[0].location, 0.0, 0.0, 0U, step_.maximum_increments, -0.05, 0.275625, 0.55, {}, {}}; if (!ParseDynamicDouble(block.data[0].fields[0], dynamic.initial_increment, block.data[0].location, block.canonical_name) || !ParseDynamicDouble(block.data[0].fields[1], dynamic.time_period, block.data[0].location, block.canonical_name) || !(dynamic.initial_increment > 0.0) || !(dynamic.time_period > 0.0)) { if (!failure_) { InputFailure( "invalid-dynamic-data", block.data[0].location, block.canonical_name, "", "Dynamic increment and time period must be finite and positive."); } return; } const double count = dynamic.time_period / dynamic.initial_increment; const double rounded_count = std::round(count); if (!std::isfinite(count) || rounded_count < 1.0 || rounded_count > static_cast(std::numeric_limits::max()) || dynamic.initial_increment * rounded_count != dynamic.time_period) { InputFailure("invalid-dynamic-schedule", block.data[0].location, block.canonical_name, "", "DYNAMIC,DIRECT requires an exact positive integer number " "of increments."); return; } dynamic.increment_count = static_cast(rounded_count); if (dynamic.increment_count > dynamic.maximum_increments) { InputFailure("invalid-dynamic-schedule", block.data[0].location, block.canonical_name, "", "DYNAMIC,DIRECT increment count cannot exceed STEP INC."); return; } dynamic.nlgeom = step_.nlgeom; step_.dynamic = std::move(dynamic); } void ParseHeatTransfer(const KeywordBlock& block) { if (model_element_family_ != ElementFamily::kThermalShell || step_.nlgeom || step_.perturbation_explicit || !ValidateParameters(block, {"END"}, "unsupported-thermal-option") || block.data.size() != 1U) { if (!failure_) InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, "", "HEAT TRANSFER requires the DS4 thermal subset."); return; } const auto* end = Parameter(block, "END"); if (end != nullptr && (!end->value || !AsciiCaseInsensitiveEquals(*end->value, "PERIOD"))) { InputFailure("unsupported-thermal-option", block.location, block.canonical_name, "END", "HEAT TRANSFER accepts only END=PERIOD."); return; } if (block.data[0].fields.size() > 5U) { 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) { InputFailure("invalid-thermal-time-grid", block.data[0].location, block.canonical_name, "", "HEAT TRANSFER requires increment and time period."); return; } if (!ParseThermalDouble(fields[0], step_.thermal_increment, block.data[0].location, block.canonical_name) || !ParseThermalDouble(fields[1], step_.thermal_period, block.data[0].location, block.canonical_name) || !(step_.thermal_increment > 0.0) || !(step_.thermal_period > 0.0)) { if (!failure_) 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; failure_ = MappingFailure{ grid.GetStatus().Category().value_or(FailureCategory::kInput), std::move(diagnostic)}; return; } step_.has_heat_transfer = true; } void ParseCflux(const KeywordBlock& block) { if (!ValidateParameters(block, {}) || block.data.empty()) { if (!failure_) 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()) { 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 (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name, true) || !ParseThermalDouble(row.fields[2], power, row.location, block.canonical_name) || (dof != 11 && dof != 15)) { 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 ParseStatic(const KeywordBlock& block) { if (model_element_family_ == ElementFamily::kSolid && Parameter(block, "DIRECT")) { if (step_.perturbation_explicit || step_.has_static || step_.frequency || step_.dynamic) { InputFailure("unsupported-analysis-procedure", block.location, block.canonical_name, step_.name_raw, "STATIC DIRECT requires one non-perturbation procedure."); return; } if (!ValidateParameters(block, {"DIRECT"})) return; const auto* direct = 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(); }))) { 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 (!ParseDouble(block.data[0].fields[0], h, block.data[0].location, "STATIC") || !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) { 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) { 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 || !ValidateParameters(block, {}) || block.data.size() != 1U || block.data[0].fields.size() != 4U) { if (!failure_) { 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) : ParseDouble(text, step_.static_values[field], block.data[0].location, block.canonical_name); if (!parsed) { return; } if (step_.static_values[field] <= 0.0) { 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]) { 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) { 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 ParseOutputRoot(const KeywordBlock& block) { if (step_.has_heat_transfer) { const auto* field = Parameter(block, "FIELD"); const auto* history = Parameter(block, "HISTORY"); const auto* variable = Parameter(block, "VARIABLE"); const auto* frequency = 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 (!RequireNoData(block)) return; if (block.parameters.size() != 2U || (!valid_field && !valid_history)) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "Thermal OUTPUT accepts FIELD, VARIABLE=PRESELECT or " "HISTORY, FREQUENCY=0."); return; } AddIgnoredWarning(block); return; } if (!ValidateParameters( block, (step_.dynamic || step_.has_heat_transfer) ? std::vector{"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"} : std::vector{"FIELD", "HISTORY", "VARIABLE"})) { return; } const auto* field = Parameter(block, "FIELD"); const auto* history = Parameter(block, "HISTORY"); const auto* variable = Parameter(block, "VARIABLE"); if ((field == nullptr) == (history == nullptr) || (field != nullptr && field->value) || (history != nullptr && history->value)) { 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"))) || !RequireNoData(block))) { if (!failure_) 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"))) { InputFailure("invalid-keyword-parameter", block.location, block.canonical_name, "VARIABLE", "Nonlinear OUTPUT requires VARIABLE=PRESELECT."); return; } if (step_.dynamic) { const auto* frequency = Parameter(block, "FREQUENCY"); if (!ValidateParameters(block, {"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) || variable == nullptr || !variable->value || !AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") || (frequency != nullptr && (!frequency->value || *frequency->value != "1")) || !RequireNoData(block)) { if (!failure_) { InputFailure( "unsupported-keyword", block.location, block.canonical_name, "", "Dynamic OUTPUT accepts only VARIABLE=PRESELECT, FREQUENCY=1."); } return; } } else if (step_.frequency && ((variable != nullptr && (!variable->value || !AsciiCaseInsensitiveEquals( *variable->value, "PRESELECT"))) || !RequireNoData(block))) { if (!failure_) { 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 ParseOutputChild(const KeywordBlock& block) { if (step_.has_heat_transfer) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "Thermal output children are outside the approved subset."); return; } if (!active_output_) { 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) { 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 = Parameter(block, "VARIABLE"); const auto* directions = 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) || !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 (!failure_) 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 = CanonicalizeAscii(field); if (!(node ? (token == "U" || token == "RF") : (token == "S" || token == "E"))) { 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 = 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_) || !ValidateParameters(block, {"VARIABLE"}) || variable == nullptr || !variable->value || !AsciiCaseInsensitiveEquals(*variable->value, required_variable) || !RequireNoData(block)) { if (!failure_) { InputFailure( "unsupported-keyword", block.location, block.canonical_name, "", "Frequency output child is outside the approved subset."); } return; } } AddIgnoredWarning(block); } void AddIgnoredWarning(const KeywordBlock& block) { // One warning per allowlisted keyword keeps no-op provenance stable; // subordinate variable rows remain attached to that keyword record. definition_.warnings.push_back( {Severity::kWarning, step_.dynamic ? "ignored-output-request" : "ignored-input-keyword", block.location, block.canonical_name, "", "The allowlisted Abaqus keyword is " "ignored without semantic effect."}); } void RejectUnknown(const KeywordBlock& block) { if (block.canonical_name == "DLOAD" && model_element_family_ == ElementFamily::kShell) { InputFailure("unsupported-distributed-load", block.location, block.canonical_name, "", "Distributed, pressure, gravity, body, edge, and follower " "loads are unsupported."); return; } InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "The keyword is outside the approved Abaqus subset."); } const RawPart* FindPart(const std::string& name) const { const auto found = std::find_if( parts_.begin(), parts_.end(), [&name](const RawPart& part) { return AsciiCaseInsensitiveEquals(part.name, name); }); return found == parts_.end() ? nullptr : &*found; } const RawInstance* FindInstance(const std::string& name) const { const auto found = std::find_if(instances_.begin(), instances_.end(), [&name](const RawInstance& instance) { return AsciiCaseInsensitiveEquals(instance.name, name); }); return found == instances_.end() ? nullptr : &*found; } std::optional FindMaterialIndex(const std::string& name) const { for (std::size_t index = 0U; index < materials_.size(); ++index) { if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) { return static_cast(index); } } return std::nullopt; } const RawSet* FindSet(const std::vector& sets, const std::string& name) const { const auto found = std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) { return AsciiCaseInsensitiveEquals(set.name, name); }); return found == sets.end() ? nullptr : &*found; } const RawNode* FindNode(const RawPart& part, std::int64_t label) const { const auto found = std::find_if( part.nodes.begin(), part.nodes.end(), [label](const RawNode& node) { return node.label == label; }); return found == part.nodes.end() ? nullptr : &*found; } const RawElement* FindElement(const RawPart& part, std::int64_t label) const { const auto found = std::find_if( part.elements.begin(), part.elements.end(), [label](const RawElement& element) { return element.label == label; }); return found == part.elements.end() ? nullptr : &*found; } void ValidateCardinality() { if (parts_.empty() || !assembly_seen_ || instances_.empty() || materials_.empty() || !step_seen_ || (!step_.has_static && !step_.frequency && !step_.dynamic && !step_.has_heat_transfer)) { const auto location = input_.blocks.empty() ? SourceLocation{input_.source_path, 0U} : input_.blocks.back().location; InputFailure("invalid-model-cardinality", location, "", "", "The model requires part, assembly, identity instance, " "material, and one supported procedure step."); } } void FinalizeMaterials() { for (const auto& material : materials_) { if (model_element_family_ == ElementFamily::kThermalShell) { if (!material.density || !material.conductivity || !material.specific_heat) { ModelFailure("missing-thermal-material-property", material.location, "MATERIAL", material.name, "DS4 materials require DENSITY, CONDUCTIVITY, and " "SPECIFIC HEAT."); return; } definition_.thermal_materials.push_back({{"", 0, material.name}, material.name, *material.conductivity, *material.density, *material.specific_heat, material.location}); continue; } if (!material.has_elastic) { InputFailure("unresolved-reference", material.location, "MATERIAL", material.name, "A material must own exactly one ELASTIC row."); return; } if (model_element_family_ == ElementFamily::kShell || model_element_family_ == ElementFamily::kSolid) { if (!(material.youngs_modulus > 0.0) || !(material.poisson_ratio > -1.0) || !(material.poisson_ratio < 0.5)) { ModelFailure(model_element_family_ == ElementFamily::kSolid ? "invalid-solid-material" : "invalid-shell-material", material.elastic_location, "ELASTIC", material.name, "Isotropic elasticity requires E>0 and -1 0.0) || !std::isfinite(shear_modulus) || !(shear_modulus > 0.0)) { ModelFailure("invalid-beam-property", material.elastic_location, "ELASTIC", material.name, "E and the derived G=E/(2*(1+nu)) must be positive."); return; } definition_.materials.push_back({material.name, material.youngs_modulus, material.poisson_ratio, material.location}); } } void FinalizeDensities() { if (model_element_family_ == ElementFamily::kThermalShell) { for (const auto& block : input_.blocks) { if (block.canonical_name != "PREPRINT") continue; if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"})) return; if (block.parameters.size() != 4U) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "Thermal PREPRINT requires four NO parameters."); return; } for (const auto& parameter : block.parameters) { if (!parameter.value || !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { InputFailure("unsupported-keyword", block.location, block.canonical_name, parameter.name, "Thermal PREPRINT parameters must equal NO."); return; } } } return; } if (physical_constants_location_) { InputFailure("unsupported-keyword", *physical_constants_location_, "PHYSICAL CONSTANTS", "", "PHYSICAL CONSTANTS is supported only by thermal input."); return; } if (!step_.frequency && !step_.dynamic && model_element_family_ == ElementFamily::kSolid) { for (std::size_t index = 0U; index < materials_.size(); ++index) { if (materials_[index].density) definition_.densities.push_back({static_cast(index), *materials_[index].density, materials_[index].density_location}); } return; } if (!step_.frequency && !step_.dynamic) { if (std::any_of(materials_.begin(), materials_.end(), [](const RawMaterial& material) { return material.density.has_value(); })) { InputFailure("invalid-density-data", step_.location, "DENSITY", "", "DENSITY is supported only by the FREQUENCY procedure."); } return; } if (step_.frequency) { ValidateFrequencyPreprint(); if (failure_) { return; } } if (step_.dynamic) { ValidateDynamicPreprint(); if (failure_) { return; } for (auto& warning : definition_.warnings) { if (warning.keyword == "PREPRINT") { warning.code = "ignored-output-request"; } } } if (model_element_family_ != ElementFamily::kShell && model_element_family_ != ElementFamily::kSolid) { InputFailure("unsupported-analysis-procedure", step_.location, step_.frequency ? "FREQUENCY" : "DYNAMIC", step_.name_raw, "The procedure requires an approved Shell or Solid model."); return; } if (!element_surfaces_.empty()) { InputFailure("unsupported-keyword", element_surfaces_.front().location, "SURFACE", element_surfaces_.front().name, "The selected procedure does not support element surfaces."); return; } for (std::size_t index = 0U; index < materials_.size(); ++index) { const RawMaterial& material = materials_[index]; if (!material.density) { bool active = false; for (const auto& section : definition_.shell_sections) { if (section.material_index == static_cast(index)) { active = true; break; } } for (const auto& section : definition_.solid_sections) { if (section.MaterialIndex() == static_cast(index)) active = true; } if (active) { ModelFailure( "missing-material-density", material.location, "MATERIAL", material.name, "Each active dynamic or frequency material requires DENSITY."); return; } continue; } definition_.densities.push_back({static_cast(index), *material.density, material.density_location}); } } void ValidateFrequencyPreprint() { for (const auto& block : input_.blocks) { if (block.canonical_name != "PREPRINT") { continue; } if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || block.parameters.size() != 4U) { if (!failure_) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "FREQUENCY requires PREPRINT with ECHO, MODEL, HISTORY, " "and CONTACT parameters."); } return; } for (const auto& parameter : block.parameters) { if (!parameter.value || !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { InputFailure("unsupported-keyword", block.location, block.canonical_name, parameter.name, "FREQUENCY requires PREPRINT parameters to equal NO."); return; } } } for (auto& warning : definition_.warnings) { if (warning.keyword == "PREPRINT") { warning.message = "mandatory-eigen-output-unchanged"; } } } void ValidateDynamicPreprint() { for (const auto& block : input_.blocks) { if (block.canonical_name != "PREPRINT") { continue; } if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) || block.parameters.size() != 4U) { if (!failure_) { InputFailure("unsupported-keyword", block.location, block.canonical_name, "", "DYNAMIC requires PREPRINT with ECHO, MODEL, HISTORY, " "and CONTACT parameters."); } return; } for (const auto& parameter : block.parameters) { if (!parameter.value || !AsciiCaseInsensitiveEquals(*parameter.value, "NO")) { InputFailure("unsupported-keyword", block.location, block.canonical_name, parameter.name, "DYNAMIC requires PREPRINT parameters to equal NO."); return; } } } } void FinalizePartsAndSections() { part_section_assignments_.resize(parts_.size()); part_shell_section_assignments_.resize(parts_.size()); const bool shell_model = model_element_family_ == ElementFamily::kShell || model_element_family_ == ElementFamily::kThermalShell; for (std::size_t part_index = 0U; part_index < parts_.size(); ++part_index) { const RawPart& part = parts_[part_index]; if (part.nodes.empty() || part.elements.empty() || (!shell_model && model_element_family_ != ElementFamily::kSolid && part.sections.empty())) { InputFailure("invalid-model-cardinality", part.location, "PART", part.name, "A part requires nodes, elements, and its approved " "section form."); return; } PartDefinition part_definition{}; part_definition.name = part.name; part_definition.location = part.location; for (const auto& node : part.nodes) { part_definition.node_source_labels.push_back(node.label); } for (const auto& element : part.elements) { part_definition.element_source_labels.push_back(element.label); for (const auto node_label : element.node_labels) { if (FindNode(part, node_label) == nullptr) { InputFailure(shell_model ? "invalid-shell-connectivity" : "unresolved-reference", element.location, "ELEMENT", element.label_text, "Element connectivity must resolve within its part."); return; } } } for (const auto& set : part.node_sets) { part_definition.node_set_names.push_back(set.name); for (const auto label : set.members) { if (FindNode(part, label) == nullptr) { InputFailure("unresolved-reference", set.location, "NSET", set.name, "Every node-set member must resolve within its part."); return; } } } for (const auto& set : part.element_sets) { part_definition.element_set_names.push_back(set.name); for (const auto label : set.members) { if (FindElement(part, label) == nullptr) { InputFailure( "unresolved-reference", set.location, "ELSET", set.name, "Every element-set member must resolve within its part."); return; } } } definition_.parts.push_back(std::move(part_definition)); if (model_element_family_ == ElementFamily::kSolid) { auto& assignments = part_shell_section_assignments_[part_index]; for (const auto& section : part.solid_sections) { const auto* element_set = FindSet(part.element_sets, section.element_set_name); const auto material = FindMaterialIndex(section.material_name); if (!element_set || !material) { InputFailure("unresolved-solid-section", section.location, "SOLID SECTION", section.element_set_name, "Solid ELSET and MATERIAL must resolve."); return; } const auto index = static_cast(definition_.solid_sections.size()); definition_.solid_sections.emplace_back( SourceEntityId{part.name, static_cast(index), section.element_set_name}, section.element_set_name, *material, section.location); for (const auto label : element_set->members) { if (!assignments.emplace(label, std::make_pair(index, *material)) .second) { InputFailure( "invalid-solid-section-assignment", section.location, "SOLID SECTION", std::to_string(label), "Solid elements require exactly one section assignment."); return; } } } for (const auto& element : part.elements) { if (assignments.find(element.label) == assignments.end()) { InputFailure("invalid-solid-section-assignment", element.location, "ELEMENT", element.label_text, "A Solid section assignment is missing."); return; } } continue; } if (shell_model) { auto& assignments = part_shell_section_assignments_[part_index]; for (const auto& section : part.shell_sections) { const auto* element_set = FindSet(part.element_sets, section.element_set_name); const auto material_index = FindMaterialIndex(section.material_name); if (element_set == nullptr || !material_index) { InputFailure("unresolved-shell-section", section.location, "SHELL SECTION", section.element_set_name, "Shell section ELSET and MATERIAL references " "must resolve."); return; } const EntityIndex section_index = model_element_family_ == ElementFamily::kThermalShell ? static_cast( definition_.thermal_sections.size()) : static_cast(definition_.shell_sections.size()); if (model_element_family_ == ElementFamily::kThermalShell) { definition_.thermal_sections.push_back( {{part.name, static_cast(section_index), section.element_set_name}, section.element_set_name, section.thickness, *material_index, section.location}); } else { definition_.shell_sections.push_back( {section.element_set_name, section.thickness, *material_index, section.location}); } for (const auto element_label : element_set->members) { if (!assignments .emplace(element_label, std::make_pair(section_index, *material_index)) .second) { InputFailure("invalid-shell-section-assignment", section.location, "SHELL SECTION", std::to_string(element_label), "A shell element cannot receive multiple " "section assignments."); return; } } } for (const auto& element : part.elements) { if (assignments.find(element.label) == assignments.end()) { InputFailure("invalid-shell-section-assignment", element.location, "ELEMENT", element.label_text, "Every shell element requires exactly one " "resolved section assignment."); return; } } continue; } auto& assignments = part_section_assignments_[part_index]; for (const auto& section : part.sections) { const auto* element_set = FindSet(part.element_sets, section.element_set_name); const auto material_index = FindMaterialIndex(section.material_name); if (element_set == nullptr || !material_index) { InputFailure("unresolved-reference", section.location, "BEAM GENERAL SECTION", section.element_set_name, "Section ELSET and MATERIAL references must resolve."); return; } if (section.properties[2] != 0.0) { ModelFailure("unsupported-coupled-section", section.location, "BEAM GENERAL SECTION", section.element_set_name, "V0 requires exact I12=0."); return; } if (!(section.properties[0] > 0.0) || !(section.properties[1] > 0.0) || !(section.properties[3] > 0.0) || !(section.properties[4] > 0.0)) { ModelFailure("invalid-beam-property", section.location, "BEAM GENERAL SECTION", section.element_set_name, "A, I11, I22, and J must be positive."); return; } const EntityIndex section_index = static_cast(definition_.sections.size()); definition_.sections.push_back( {section.element_set_name, section.properties[0], section.properties[1], section.properties[2], section.properties[3], section.properties[4], section.first_axis, section.section_points, section.location}); for (const auto element_label : element_set->members) { if (!assignments .emplace(element_label, std::make_pair(section_index, *material_index)) .second) { InputFailure("duplicate-entity", section.location, "BEAM GENERAL SECTION", std::to_string(element_label), "An element cannot receive multiple section " "assignments."); return; } } } for (const auto& element : part.elements) { if (assignments.find(element.label) == assignments.end()) { InputFailure( "unresolved-reference", element.location, "ELEMENT", element.label_text, "Every B33 element requires a resolved section assignment."); return; } } } } std::size_t PartIndex(const RawPart& part) const { return static_cast(&part - parts_.data()); } bool ValidateGeometry(const RawElement& raw, const Node& first, const Node& second, const GeneralBeamSection& section) { const auto maximum_absolute = [](const Vector3& vector) { return std::max( {std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])}); }; const Vector3 first_position{first.coordinates}; const Vector3 second_position{second.coordinates}; // Compare both approved inequalities after a common scaling. This // preserves the exact ratios while avoiding overflow in x*x and in // subtraction between large finite coordinates. const double global_coordinate_scale = std::max({1.0, maximum_absolute(first_position), maximum_absolute(second_position)}); const Vector3 first_scaled = first_position / global_coordinate_scale; const Vector3 second_scaled = second_position / global_coordinate_scale; const Vector3 delta_scaled = second_scaled - first_scaled; const double length_ratio = delta_scaled.Norm(); const double coordinate_norm_ratio = std::max({1.0 / global_coordinate_scale, first_scaled.Norm(), second_scaled.Norm()}); if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) { return ModelFailure( "invalid-beam-length", raw.location, "ELEMENT", raw.label_text, "Beam length fails the approved scale-aware threshold."); } const Vector3 tangent = delta_scaled / length_ratio; const Vector3 guide{section.first_axis}; const double global_guide_scale = std::max(1.0, maximum_absolute(guide)); const Vector3 guide_scaled = guide / global_guide_scale; const double projection = guide_scaled.Dot(tangent); const Vector3 perpendicular = guide_scaled - projection * tangent; const double guide_norm_ratio = std::max(1.0 / global_guide_scale, guide_scaled.Norm()); if (!(perpendicular.Norm() > 1.0e-12 * guide_norm_ratio)) { return ModelFailure( "invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION", raw.label_text, "The first section axis cannot be zero or tangent-parallel."); } return true; } void ExpandInstances() { for (const auto& raw_instance : instances_) { const RawPart* part = FindPart(raw_instance.part_name); if (part == nullptr) { InputFailure("unresolved-reference", raw_instance.location, "INSTANCE", raw_instance.name, "INSTANCE PART must resolve case-insensitively."); return; } InstanceDefinition instance{ raw_instance.name, part->name, {}, {}, raw_instance.location}; std::map node_indices; std::map element_indices; // Instance order, followed by part-local declaration order, is the // sole source of stable expanded internal IDs. for (const auto& raw_node : part->nodes) { if (definition_.nodes.size() > std::numeric_limits::max()) { InputFailure("entity-index-overflow", raw_node.location, "NODE", raw_node.label_text, "Expanded node count exceeds EntityIndex capacity."); return; } const EntityIndex index = static_cast(definition_.nodes.size()); definition_.nodes.push_back( {{raw_instance.name, raw_node.label, raw_node.label_text}, raw_node.coordinates, raw_node.location}); node_indices.emplace(raw_node.label, index); instance.node_mappings.push_back({raw_node.label, index}); } for (const auto& raw_element : part->elements) { EntityIndex index = 0U; if (raw_element.type == RawElement::Type::kB33) { const auto first = node_indices.find(raw_element.node_labels[0]); const auto second = node_indices.find(raw_element.node_labels[1]); const auto& assignments = part_section_assignments_[PartIndex(*part)]; const auto assignment = assignments.find(raw_element.label); if (first == node_indices.end() || second == node_indices.end() || assignment == assignments.end()) { InputFailure( "unresolved-reference", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded connectivity and section assignment must resolve."); return; } if (definition_.elements.size() > std::numeric_limits::max()) { InputFailure( "entity-index-overflow", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded element count exceeds EntityIndex capacity."); return; } index = static_cast(definition_.elements.size()); const auto& section = definition_.sections[assignment->second.first]; if (!ValidateGeometry(raw_element, definition_.nodes[first->second], definition_.nodes[second->second], section)) { return; } definition_.elements.push_back( {{raw_instance.name, raw_element.label, raw_element.label_text}, {first->second, second->second}, assignment->second.second, assignment->second.first, raw_element.location}); } else if (raw_element.type == RawElement::Type::kC3d8) { std::array connected_nodes{}; for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { const auto found = node_indices.find(raw_element.node_labels[node]); if (found == node_indices.end()) { InputFailure("invalid-solid-connectivity", raw_element.location, "ELEMENT", raw_element.label_text, "Solid nodes must resolve."); return; } connected_nodes[node] = found->second; } const auto& assignments = part_shell_section_assignments_[PartIndex(*part)]; const auto assignment = assignments.find(raw_element.label); if (assignment == assignments.end() || definition_.solid_elements.size() > std::numeric_limits::max()) { InputFailure("invalid-solid-section-assignment", raw_element.location, "ELEMENT", raw_element.label_text, "Solid assignment must resolve."); return; } index = static_cast(definition_.solid_elements.size()); definition_.solid_elements.emplace_back( SourceEntityId{raw_instance.name, raw_element.label, raw_element.label_text}, connected_nodes, assignment->second.second, assignment->second.first, raw_element.location); } else { std::array connected_nodes{}; for (std::size_t node = 0U; node < connected_nodes.size(); ++node) { const auto found = node_indices.find(raw_element.node_labels[node]); if (found == node_indices.end()) { InputFailure("invalid-shell-connectivity", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded shell connectivity must resolve."); return; } connected_nodes[node] = found->second; } const auto& assignments = part_shell_section_assignments_[PartIndex(*part)]; const auto assignment = assignments.find(raw_element.label); if (assignment == assignments.end()) { InputFailure( "invalid-shell-section-assignment", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded shell section assignment must resolve exactly once."); return; } if (raw_element.type == RawElement::Type::kDs4) { if (definition_.thermal_elements.size() > std::numeric_limits::max()) { InputFailure("entity-index-overflow", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded thermal element count exceeds EntityIndex " "capacity."); return; } index = static_cast(definition_.thermal_elements.size()); definition_.thermal_elements.push_back( {{raw_instance.name, raw_element.label, raw_element.label_text}, connected_nodes, assignment->second.first, raw_element.location}); element_indices.emplace(raw_element.label, index); instance.element_mappings.push_back({raw_element.label, index}); continue; } if (definition_.shell_elements.size() > std::numeric_limits::max()) { InputFailure( "entity-index-overflow", raw_element.location, "ELEMENT", raw_element.label_text, "Expanded shell element count exceeds EntityIndex capacity."); return; } index = static_cast(definition_.shell_elements.size()); definition_.shell_elements.push_back( {{raw_instance.name, raw_element.label, raw_element.label_text}, raw_element.type == RawElement::Type::kS4 ? ShellSourceElementType::kS4 : ShellSourceElementType::kS4r, connected_nodes, assignment->second.second, assignment->second.first, raw_element.location}); } element_indices.emplace(raw_element.label, index); instance.element_mappings.push_back({raw_element.label, index}); } for (const auto& raw_set : part->node_sets) { NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; for (const auto member : raw_set.members) { const auto found = node_indices.find(member); if (found == node_indices.end()) { InputFailure("unresolved-reference", raw_set.location, "NSET", raw_set.name, "Part node-set expansion failed."); return; } set.node_indices.push_back(found->second); } definition_.node_sets.push_back(std::move(set)); } for (const auto& raw_set : part->element_sets) { ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location}; for (const auto member : raw_set.members) { const auto found = element_indices.find(member); if (found == element_indices.end()) { InputFailure("unresolved-reference", raw_set.location, "ELSET", raw_set.name, "Part element-set expansion failed."); return; } set.element_indices.push_back(found->second); } definition_.element_sets.push_back(std::move(set)); } definition_.instances.push_back(std::move(instance)); } } void ExpandAssemblySets() { for (const auto& raw_set : assembly_sets_) { const RawInstance* raw_instance = FindInstance(raw_set.instance_name); if (raw_instance == nullptr) { InputFailure("unresolved-reference", raw_set.location, raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, "Assembly set INSTANCE must resolve."); return; } const auto definition_instance = std::find_if( definition_.instances.begin(), definition_.instances.end(), [&raw_instance](const InstanceDefinition& instance) { return AsciiCaseInsensitiveEquals(instance.name, raw_instance->name); }); if (definition_instance == definition_.instances.end()) { InputFailure("unresolved-reference", raw_set.location, raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name, "Assembly set instance expansion is unavailable."); return; } if (raw_set.is_node_set) { definition_.node_sets.erase( std::remove_if( definition_.node_sets.begin(), definition_.node_sets.end(), [&raw_set](const NodeSet& set) { return AsciiCaseInsensitiveEquals(set.name, raw_set.name); }), definition_.node_sets.end()); NodeSet set{ raw_set.name, definition_instance->name, {}, raw_set.location}; for (const auto member : raw_set.members) { const auto mapping = std::find_if(definition_instance->node_mappings.begin(), definition_instance->node_mappings.end(), [member](const SourceIndexMapping& value) { return value.source_label == member; }); if (mapping == definition_instance->node_mappings.end()) { InputFailure("unresolved-reference", raw_set.location, "NSET", raw_set.name, "Assembly node-set member must resolve in its " "instance."); return; } set.node_indices.push_back(mapping->internal_index); } definition_.node_sets.push_back(std::move(set)); } else { definition_.element_sets.erase( std::remove_if(definition_.element_sets.begin(), definition_.element_sets.end(), [&raw_set](const ElementSet& set) { return AsciiCaseInsensitiveEquals(set.name, raw_set.name); }), definition_.element_sets.end()); ElementSet set{ raw_set.name, definition_instance->name, {}, raw_set.location}; for (const auto member : raw_set.members) { const auto mapping = std::find_if(definition_instance->element_mappings.begin(), definition_instance->element_mappings.end(), [member](const SourceIndexMapping& value) { return value.source_label == member; }); if (mapping == definition_instance->element_mappings.end()) { InputFailure("unresolved-reference", raw_set.location, "ELSET", raw_set.name, "Assembly element-set member must resolve in " "its instance."); return; } set.element_indices.push_back(mapping->internal_index); } definition_.element_sets.push_back(std::move(set)); } } } bool AppendSurfaceFacet( const RawSurfaceMember& member, const RawInstance& raw_instance, std::size_t instance_ordinal, EntityIndex element_index, SurfaceMemberKind member_kind, const std::string& elset_name_raw, std::vector>* candidates) { const bool thermal = model_element_family_ == ElementFamily::kThermalShell; const auto element_count = thermal ? definition_.thermal_elements.size() : definition_.shell_elements.size(); if (static_cast(element_index) >= element_count) { return InputFailure( "unsupported-element-formulation", member.location, "SURFACE", member.target, "Element surfaces may resolve only to S4 or S4R shell elements."); } const RawPart* part = FindPart(raw_instance.part_name); if (part == nullptr) { return InputFailure( "unresolved-reference", member.location, "SURFACE", member.target, "The surface instance must resolve to its source part."); } const auto& source_id = thermal ? definition_.thermal_elements[element_index].source_id : definition_.shell_elements[element_index].source_id; const std::string source_type = thermal ? "DS4" : (definition_.shell_elements[element_index].source_type == ShellSourceElementType::kS4 ? "S4" : "S4R"); ElementSurfaceFacetDefinition facet{element_index, 0U, part->name, CanonicalizeAscii(part->name), raw_instance.name, CanonicalizeAscii(raw_instance.name), source_id.source_label, source_id.source_label_text, source_type, member.side, member.location, member.ordinal, member_kind, elset_name_raw, member_kind == SurfaceMemberKind::kElset ? CanonicalizeAscii(elset_name_raw) : ""}; candidates->push_back({std::move(facet), instance_ordinal}); return true; } bool ResolveDirectSurfaceMember( const RawSurfaceMember& member, std::vector>* candidates) { const std::size_t dot = member.target.find('.'); if (dot == std::string::npos || dot == 0U || dot + 1U >= member.target.size() || member.target.find('.', dot + 1U) != std::string::npos) { return InputFailure( "unresolved-reference", member.location, "SURFACE", member.target, "A direct surface member must be instance.positive-element-label."); } const std::string instance_name = member.target.substr(0U, dot); if (!IsPressureNameToken(instance_name)) { return InputFailure( "unsupported-surface-target", member.location, "SURFACE", member.target, "A direct surface member must use an identity instance name."); } std::int64_t source_label = 0; if (!ParseInteger(member.target.substr(dot + 1U), source_label, member.location, "SURFACE", true)) { return false; } const auto raw_instance = std::find_if( instances_.begin(), instances_.end(), [&instance_name](const RawInstance& value) { return AsciiCaseInsensitiveEquals(value.name, instance_name); }); if (raw_instance == instances_.end()) { if (FindPart(instance_name) != nullptr) { return InputFailure( "unsupported-surface-target", member.location, "SURFACE", member.target, "A direct surface member cannot use a part name as an instance."); } return InputFailure("unresolved-reference", member.location, "SURFACE", member.target, "A direct surface member instance must resolve."); } const auto instance = std::find_if( definition_.instances.begin(), definition_.instances.end(), [&raw_instance](const InstanceDefinition& value) { return AsciiCaseInsensitiveEquals(value.name, raw_instance->name); }); const auto element = std::find_if( instance->element_mappings.begin(), instance->element_mappings.end(), [source_label](const SourceIndexMapping& value) { return value.source_label == source_label; }); if (element == instance->element_mappings.end()) { return InputFailure( "unresolved-reference", member.location, "SURFACE", member.target, "A direct surface member must resolve to a supported shell element."); } return AppendSurfaceFacet(member, *raw_instance, static_cast(std::distance( instances_.begin(), raw_instance)), element->internal_index, SurfaceMemberKind::kDirect, "", candidates); } bool ResolveElsetSurfaceMember( const RawSurfaceMember& member, std::vector>* candidates) { if (!IsPressureNameToken(member.target)) { return InputFailure("unsupported-surface-target", member.location, "SURFACE", member.target, "A surface ELSET must use the approved name syntax."); } const auto raw_set = std::find_if( assembly_sets_.begin(), assembly_sets_.end(), [&member](const RawAssemblySet& value) { return !value.is_node_set && AsciiCaseInsensitiveEquals(value.name, member.target); }); if (raw_set == assembly_sets_.end()) { const bool part_scoped = std::any_of( parts_.begin(), parts_.end(), [this, &member](const RawPart& part) { return FindSet(part.element_sets, member.target) != nullptr; }); if (part_scoped) { return InputFailure( "cross-scope-target", member.location, "SURFACE", member.target, "A surface ELSET must be declared at assembly scope."); } return InputFailure( "missing-reference", member.location, "SURFACE", member.target, "A surface ELSET must be assembly-scoped and explicit-instance."); } const auto raw_instance = std::find_if( instances_.begin(), instances_.end(), [&raw_set](const RawInstance& value) { return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name); }); if (raw_instance == instances_.end()) { return InputFailure("unresolved-reference", member.location, "SURFACE", member.target, "The surface ELSET instance must resolve."); } const auto element_set = std::find_if( definition_.element_sets.begin(), definition_.element_sets.end(), [&raw_set](const ElementSet& value) { return AsciiCaseInsensitiveEquals(value.name, raw_set->name) && value.instance_name && AsciiCaseInsensitiveEquals(*value.instance_name, raw_set->instance_name); }); if (element_set == definition_.element_sets.end()) { return InputFailure( "unresolved-reference", member.location, "SURFACE", member.target, "The surface ELSET must expand in its explicit instance."); } for (const EntityIndex element_index : element_set->element_indices) { if (!AppendSurfaceFacet(member, *raw_instance, static_cast(std::distance( instances_.begin(), raw_instance)), element_index, SurfaceMemberKind::kElset, member.target, candidates)) { return false; } } return true; } void ResolveElementSurfaces() { std::vector resolved_surfaces; resolved_surfaces.reserve(element_surfaces_.size()); for (const auto& surface : element_surfaces_) { std::vector> candidates; for (const auto& member : surface.members) { const bool is_direct = member.target.find('.') != std::string::npos; if (!(is_direct ? ResolveDirectSurfaceMember(member, &candidates) : ResolveElsetSurfaceMember(member, &candidates))) { return; } } if (candidates.empty()) { InputFailure("empty-surface", surface.location, "SURFACE", surface.name, "An element surface must resolve at least one facet."); return; } std::vector> unique_facets; for (auto& candidate : candidates) { const auto same_element = std::find_if(unique_facets.begin(), unique_facets.end(), [&candidate](const auto& existing) { return existing.first.instance_name_canonical == candidate.first.instance_name_canonical && existing.first.source_element_label == candidate.first.source_element_label; }); if (same_element != unique_facets.end()) { if (same_element->first.side != candidate.first.side) { InputFailure( "conflicting-surface-membership", candidate.first.first_member_location, "SURFACE", surface.name, "One shell element cannot appear on both surface sides."); return; } if (model_element_family_ != ElementFamily::kThermalShell) { definition_.warnings.push_back( {Severity::kWarning, "duplicate-surface-member-normalized", candidate.first.first_member_location, "SURFACE", surface.name, "A repeated same-side surface member was normalized once."}); } continue; } unique_facets.push_back(std::move(candidate)); } std::sort(unique_facets.begin(), unique_facets.end(), [](const auto& lhs, const auto& rhs) { const int lhs_side = lhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; const int rhs_side = rhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1; return std::tie(lhs.second, lhs.first.source_element_label, lhs_side) < std::tie(rhs.second, rhs.first.source_element_label, rhs_side); }); ElementSurfaceDefinition resolved{surface.name, CanonicalizeAscii(surface.name), {}, surface.location, surface.declaration_ordinal}; resolved.facets.reserve(unique_facets.size()); for (std::size_t ordinal = 0U; ordinal < unique_facets.size(); ++ordinal) { unique_facets[ordinal].first.facet_ordinal = ordinal; resolved.facets.push_back(std::move(unique_facets[ordinal].first)); } resolved_surfaces.push_back(std::move(resolved)); } definition_.element_surfaces = std::move(resolved_surfaces); } /// @brief Builds one stable node-target index from the completed candidate. SourceTargetIndex BuildSourceTargetIndex() const { std::vector entries; std::size_t declaration_order = 0U; for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) { const auto& source_id = definition_.nodes[node].source_id; entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "", source_id, static_cast(node), declaration_order++}); } for (const auto& set : definition_.node_sets) { for (const EntityIndex node : set.node_indices) { const auto& source_id = definition_.nodes[node].source_id; entries.push_back({SourceEntityKind::kNode, set.instance_name.value_or(source_id.instance_name), set.name, source_id, node, declaration_order++}); } } return SourceTargetIndex{std::move(entries)}; } std::optional> 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()) { 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]) { 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 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 = BuildSourceTargetIndex(); 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 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) { 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 (failure_) { return; } } if (step_.dynamic) { ValidateDynamicBoundaries(boundaries); if (failure_) { return; } } const SourceTargetIndex target_index = BuildSourceTargetIndex(); 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) { 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]) { 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) { 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()) { 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, CanonicalizeAscii(load.surface_name), "P", load.magnitude, load.magnitude_raw, load.keyword_location, load.data_location, load.data_ordinal, load.load_ordinal}); } // Static time-control values are provenance only: V0 creates exactly // the canonical final frame 0 and performs no increment loop here. definition_.steps.push_back( {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; } const ParsedInput& input_; ModelDefinition definition_{}; std::optional failure_; std::vector parts_; std::vector instances_; std::vector assembly_sets_; std::vector element_surfaces_; std::vector materials_; std::vector model_boundaries_; std::vector thermal_model_boundaries_; std::vector thermal_initial_conditions_; std::vector thermal_active_nodes_; RawStep step_{}; std::vector>> part_section_assignments_; std::vector>> part_shell_section_assignments_; std::optional current_part_; std::optional pending_section_; std::optional material_eligible_; std::optional model_element_family_; bool heading_seen_{false}; bool part_elements_seen_{false}; bool part_sets_seen_{false}; bool part_sections_seen_{false}; bool beam_section_context_active_{false}; bool assembly_seen_{false}; bool assembly_set_seen_{false}; bool model_boundary_seen_{false}; bool in_assembly_{false}; bool in_instance_{false}; bool step_seen_{false}; bool in_step_{false}; bool step_load_seen_{false}; bool step_no_op_seen_{false}; bool active_output_{false}; bool active_output_is_field_{false}; std::optional physical_constants_location_; }; DomainMappingContext::DomainMappingContext(const ParsedInput& input) : impl_{std::make_unique(input)} {} DomainMappingContext::~DomainMappingContext() = default; DomainMappingCandidateSummary DomainMappingContext::CandidateSummary() const noexcept { return impl_->CandidateSummary(); } Status DomainMappingContext::ParseSyntax() { return impl_->ParseSyntax(); } Status DomainMappingContext::FinalizeMaterialProperties() { return impl_->FinalizeMaterialProperties(); } Status DomainMappingContext::FinalizeTopology() { return impl_->FinalizeTopology(); } Status DomainMappingContext::FinalizeElementSurfaces() { return impl_->FinalizeElementSurfaces(); } Status DomainMappingContext::FinalizeStep() { return impl_->FinalizeStepCandidate(); } Result DomainMappingContext::Commit() { return impl_->Commit(); } } // namespace fesa::abaqus_internal