#pragma once #include "fesa/Core/Core.hpp" #include "fesa/Util/Util.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace fesa { struct KeywordLine { std::string name; std::map parameters; std::set flags; }; inline KeywordLine parseKeywordLine(const std::string& line) { KeywordLine keyword; std::vector pieces = splitCsv(line.substr(1)); if (pieces.empty()) { return keyword; } keyword.name = lower(trim(pieces.front())); for (std::size_t i = 1; i < pieces.size(); ++i) { const std::string piece = trim(pieces[i]); if (piece.empty()) { continue; } const auto eq = piece.find('='); if (eq == std::string::npos) { keyword.flags.insert(lower(piece)); } else { keyword.parameters[lower(trim(piece.substr(0, eq)))] = trim(piece.substr(eq + 1)); } } return keyword; } struct ParseResult { Domain domain; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; class AbaqusInputParser { public: ParseResult parseString(const std::string& text, const std::string& file_name = "") const { ParseResult result; std::istringstream stream(text); std::string line; KeywordLine current; std::string current_material_key; KeywordLine current_shell_section; LocalIndex line_number = 0; LocalIndex current_keyword_line = 0; auto add_error = [&](const std::string& code, const std::string& message) { const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line; result.diagnostics.push_back({Severity::Error, code, message, {file_name, source_line, current.name}}); }; auto is_allowed = [](const std::string& value, std::initializer_list allowed_values) { return std::any_of(allowed_values.begin(), allowed_values.end(), [&](const char* allowed) { return value == allowed; }); }; auto reject_unsupported_controls = [&](std::initializer_list allowed_parameters, std::initializer_list allowed_flags) { for (const auto& [parameter, value] : current.parameters) { (void)value; if (!is_allowed(parameter, allowed_parameters)) { const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line; result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-PARAMETER", "Unsupported *" + current.name + " parameter: " + parameter, {file_name, source_line, current.name}}); } } for (const std::string& flag : current.flags) { if (!is_allowed(flag, allowed_flags)) { const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line; result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-PARAMETER", "Unsupported *" + current.name + " flag: " + flag, {file_name, source_line, current.name}}); } } }; while (std::getline(stream, line)) { ++line_number; line = trim(line); if (line.empty() || line.rfind("**", 0) == 0) { continue; } if (!line.empty() && line.front() == '*') { current_keyword_line = line_number; std::string keyword_line = line; while (!keyword_line.empty() && keyword_line.back() == ',') { std::string continuation; if (!std::getline(stream, continuation)) { break; } ++line_number; continuation = trim(continuation); if (continuation.empty() || continuation.rfind("**", 0) == 0) { continue; } keyword_line += continuation; } current = parseKeywordLine(keyword_line); if (current.name == "node") { reject_unsupported_controls({}, {}); continue; } if (current.name == "element") { reject_unsupported_controls({"type", "elset"}, {}); continue; } if (current.name == "nset") { reject_unsupported_controls({"nset"}, {"generate"}); continue; } if (current.name == "elset") { reject_unsupported_controls({"elset"}, {"generate"}); continue; } if (current.name == "elastic") { reject_unsupported_controls({}, {}); continue; } if (current.name == "shell section") { reject_unsupported_controls({"elset", "material"}, {}); current_shell_section = current; continue; } if (current.name == "boundary" || current.name == "cload" || current.name == "static") { reject_unsupported_controls({}, {}); continue; } if (current.name == "material") { reject_unsupported_controls({"name"}, {}); auto name_it = current.parameters.find("name"); if (name_it == current.parameters.end() || trim(name_it->second).empty()) { add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME"); current_material_key.clear(); continue; } Material material; material.name = trim(name_it->second); current_material_key = Domain::key(material.name); if (result.domain.materials.count(current_material_key) != 0) { add_error("FESA-PARSE-DUPLICATE-MATERIAL", "Duplicate material: " + material.name); } else { result.domain.materials[current_material_key] = material; } continue; } if (current.name == "step") { reject_unsupported_controls({"name", "nlgeom"}, {}); auto nlgeom = current.parameters.find("nlgeom"); if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") { add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1"); } StepDefinition step; auto name_it = current.parameters.find("name"); if (name_it != current.parameters.end() && !trim(name_it->second).empty()) { step.name = trim(name_it->second); } result.domain.steps.push_back(step); continue; } if (current.name == "end step") { reject_unsupported_controls({}, {}); continue; } add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name); continue; } const std::vector fields = splitCsv(line); if (current.name == "node") { parseNode(fields, result, file_name, line_number); } else if (current.name == "element") { parseElement(fields, current, result, file_name, line_number); } else if (current.name == "nset") { parseNodeSet(fields, current, result, file_name, line_number); } else if (current.name == "elset") { parseElementSet(fields, current, result, file_name, line_number); } else if (current.name == "elastic") { parseElastic(fields, current_material_key, result, file_name, line_number); } else if (current.name == "shell section") { parseShellSection(fields, current_shell_section, result, file_name, line_number); } else if (current.name == "boundary") { parseBoundary(fields, result, file_name, line_number); } else if (current.name == "cload") { parseLoad(fields, result, file_name, line_number); } } if (result.domain.steps.empty()) { result.domain.steps.push_back({"Step-1", "linear_static"}); } return result; } ParseResult parseFile(const std::string& path) const { std::ifstream input(path); std::ostringstream buffer; buffer << input.rdbuf(); ParseResult result = parseString(buffer.str(), path); if (!input.good() && buffer.str().empty()) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-FILE", "Could not read input file", {path, 0, ""}}); } return result; } private: static std::size_t effectiveFieldCount(const std::vector& fields) { std::size_t count = fields.size(); while (count > 0 && trim(fields[count - 1]).empty()) { --count; } return count; } static void parseNode(const std::vector& fields, ParseResult& result, const std::string& file_name, LocalIndex line) { if (effectiveFieldCount(fields) != 4) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}}); return; } auto id = parseInt64(fields[0]); auto x = parseReal(fields[1]); auto y = parseReal(fields[2]); auto z = parseReal(fields[3]); if (!id || !x || !y || !z) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-NODE-NUMERIC", "Invalid node numeric field", {file_name, line, "node"}}); return; } if (result.domain.nodes.count(*id) != 0) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-DUPLICATE-NODE", "Duplicate node id", {file_name, line, "node"}}); return; } result.domain.nodes[*id] = {*id, {*x, *y, *z}}; } static void parseElement(const std::vector& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) { auto type_it = keyword.parameters.find("type"); if (type_it == keyword.parameters.end()) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-ELEMENT-TYPE", "*Element requires TYPE", {file_name, line, "element"}}); return; } const std::string type = lower(trim(type_it->second)); if (type != "s4") { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-ELEMENT", "Unsupported element type: " + type_it->second, {file_name, line, "element"}}); return; } if (effectiveFieldCount(fields) != 5) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT", "S4 element requires id,n1,n2,n3,n4", {file_name, line, "element"}}); return; } auto id = parseInt64(fields[0]); std::array nodes{}; bool ok = id.has_value(); for (int i = 0; i < 4; ++i) { auto node = parseInt64(fields[1 + static_cast(i)]); ok = ok && node.has_value(); if (node) { nodes[static_cast(i)] = *node; } } if (!ok) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-ELEMENT-NUMERIC", "Invalid element numeric field", {file_name, line, "element"}}); return; } if (result.domain.elements.count(*id) != 0) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-DUPLICATE-ELEMENT", "Duplicate element id", {file_name, line, "element"}}); return; } Element element; element.id = *id; element.node_ids = nodes; auto elset_it = keyword.parameters.find("elset"); if (elset_it != keyword.parameters.end()) { element.source_elset = trim(elset_it->second); auto& set = result.domain.element_sets[Domain::key(element.source_elset)]; set.name = element.source_elset; addUnique(set.element_ids, *id); } result.domain.elements[*id] = element; } static void parseNodeSet(const std::vector& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) { auto name_it = keyword.parameters.find("nset"); if (name_it == keyword.parameters.end()) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-NSET-NAME", "*Nset requires NSET", {file_name, line, "nset"}}); return; } auto& set = result.domain.node_sets[Domain::key(name_it->second)]; set.name = trim(name_it->second); parseSetData(fields, keyword.flags.count("generate") != 0, set.node_ids, result.diagnostics, file_name, line, "nset"); } static void parseElementSet(const std::vector& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) { auto name_it = keyword.parameters.find("elset"); if (name_it == keyword.parameters.end()) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-ELSET-NAME", "*Elset requires ELSET", {file_name, line, "elset"}}); return; } auto& set = result.domain.element_sets[Domain::key(name_it->second)]; set.name = trim(name_it->second); parseSetData(fields, keyword.flags.count("generate") != 0, set.element_ids, result.diagnostics, file_name, line, "elset"); } static void parseSetData(const std::vector& fields, bool generate, std::vector& output, std::vector& diagnostics, const std::string& file_name, LocalIndex line, const std::string& keyword) { if (generate) { const std::size_t field_count = effectiveFieldCount(fields); if (field_count != 3) { diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Generated set requires first,last,increment", {file_name, line, keyword}}); return; } auto first = parseInt64(fields[0]); auto last = parseInt64(fields[1]); auto increment = parseInt64(fields[2]); if (!first || !last || !increment || *increment <= 0) { diagnostics.push_back( {Severity::Error, "FESA-PARSE-GENERATE", "Invalid generated set range", {file_name, line, keyword}}); return; } for (GlobalId value : generatedRange(*first, *last, *increment)) { addUnique(output, value); } return; } const std::size_t field_count = effectiveFieldCount(fields); for (std::size_t i = 0; i < field_count; ++i) { const std::string& field = fields[i]; if (trim(field).empty()) { continue; } auto value = parseInt64(field); if (!value) { diagnostics.push_back( {Severity::Error, "FESA-PARSE-SET-NUMERIC", "Invalid set id", {file_name, line, keyword}}); return; } addUnique(output, *value); } } static void parseElastic(const std::vector& fields, const std::string& material_key, ParseResult& result, const std::string& file_name, LocalIndex line) { if (material_key.empty() || result.domain.materials.count(material_key) == 0) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}}); return; } const std::size_t field_count = effectiveFieldCount(fields); if (field_count < 2) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}}); return; } if (field_count > 2) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-UNSUPPORTED", "Only isotropic E,nu elastic data is supported", {file_name, line, "elastic"}}); return; } auto e = parseReal(fields[0]); auto nu = parseReal(fields[1]); if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-RANGE", "Invalid isotropic elastic constants", {file_name, line, "elastic"}}); return; } result.domain.materials[material_key].elastic_modulus = *e; result.domain.materials[material_key].poisson_ratio = *nu; } static void parseShellSection(const std::vector& fields, const KeywordLine& keyword, ParseResult& result, const std::string& file_name, LocalIndex line) { auto elset_it = keyword.parameters.find("elset"); auto material_it = keyword.parameters.find("material"); if (elset_it == keyword.parameters.end() || material_it == keyword.parameters.end()) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-PARAM", "*Shell Section requires ELSET and MATERIAL", {file_name, line, "shell section"}}); return; } const std::size_t field_count = effectiveFieldCount(fields); if (field_count == 0) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION", "*Shell Section requires thickness", {file_name, line, "shell section"}}); return; } if (field_count > 1) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-UNSUPPORTED", "Only homogeneous shell thickness data is supported", {file_name, line, "shell section"}}); return; } auto thickness = parseReal(fields[0]); if (!thickness || *thickness <= 0.0) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-THICKNESS", "Shell thickness must be positive", {file_name, line, "shell section"}}); return; } result.domain.shell_sections.push_back({trim(elset_it->second), trim(material_it->second), *thickness}); } static void parseBoundary(const std::vector& fields, ParseResult& result, const std::string& file_name, LocalIndex line) { const std::size_t field_count = effectiveFieldCount(fields); if (field_count < 2) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}}); return; } if (field_count > 4) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-UNSUPPORTED", "Only direct zero-valued boundary data is supported", {file_name, line, "boundary"}}); return; } auto first = parseInt64(fields[1]); auto last = field_count >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first; auto magnitude = field_count >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional(0.0); if (!first || !last || !magnitude || !dofFromAbaqus(static_cast(*first)) || !dofFromAbaqus(static_cast(*last)) || *first > *last) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-DOF", "Invalid boundary DOF range", {file_name, line, "boundary"}}); return; } if (std::fabs(*magnitude) > 0.0) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-NONZERO", "Nonzero prescribed displacement is not supported in Phase 1", {file_name, line, "boundary"}}); return; } result.domain.boundary_conditions.push_back({trim(fields[0]), static_cast(*first), static_cast(*last), *magnitude}); } static void parseLoad(const std::vector& fields, ParseResult& result, const std::string& file_name, LocalIndex line) { const std::size_t field_count = effectiveFieldCount(fields); if (field_count < 3) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}}); return; } if (field_count > 3) { result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD-UNSUPPORTED", "Only direct concentrated load data is supported", {file_name, line, "cload"}}); return; } auto dof = parseInt64(fields[1]); auto magnitude = parseReal(fields[2]); if (!dof || !magnitude || !dofFromAbaqus(static_cast(*dof))) { result.diagnostics.push_back( {Severity::Error, "FESA-PARSE-CLOAD-DOF", "Invalid concentrated load", {file_name, line, "cload"}}); return; } result.domain.loads.push_back({trim(fields[0]), static_cast(*dof), *magnitude}); } }; } // namespace fesa