refactor: extract abaqus input parser

This commit is contained in:
NINI
2026-05-05 01:31:02 +09:00
parent 34e7d1638f
commit 339bf1cbb9
8 changed files with 651 additions and 448 deletions
+542
View File
@@ -0,0 +1,542 @@
#pragma once
#include "fesa/Core/Core.hpp"
#include "fesa/Util/Util.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <fstream>
#include <initializer_list>
#include <map>
#include <optional>
#include <set>
#include <sstream>
#include <string>
#include <vector>
namespace fesa {
struct KeywordLine {
std::string name;
std::map<std::string, std::string> parameters;
std::set<std::string> flags;
};
inline KeywordLine parseKeywordLine(const std::string& line) {
KeywordLine keyword;
std::vector<std::string> 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<Diagnostic> diagnostics;
bool ok() const {
return !hasError(diagnostics);
}
};
class AbaqusInputParser {
public:
ParseResult parseString(const std::string& text, const std::string& file_name = "<memory>") 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<const char*> 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<const char*> allowed_parameters,
std::initializer_list<const char*> 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<std::string> 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<std::string>& fields) {
std::size_t count = fields.size();
while (count > 0 && trim(fields[count - 1]).empty()) {
--count;
}
return count;
}
static void parseNode(const std::vector<std::string>& 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<std::string>& 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<GlobalId, 4> nodes{};
bool ok = id.has_value();
for (int i = 0; i < 4; ++i) {
auto node = parseInt64(fields[1 + static_cast<std::size_t>(i)]);
ok = ok && node.has_value();
if (node) {
nodes[static_cast<std::size_t>(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<std::string>& 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<std::string>& 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<std::string>& fields,
bool generate,
std::vector<GlobalId>& output,
std::vector<Diagnostic>& 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<std::string>& 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<std::string>& 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<std::string>& 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<Real>(0.0);
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) ||
!dofFromAbaqus(static_cast<int>(*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<int>(*first), static_cast<int>(*last), *magnitude});
}
static void parseLoad(const std::vector<std::string>& 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<int>(*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<int>(*dof), *magnitude});
}
};
} // namespace fesa
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include "fesa/IO/AbaqusInputParser.hpp"
#include "fesa/ModuleInfo.hpp"
namespace fesa::module {
+1 -443
View File
@@ -2,6 +2,7 @@
#include "fesa/Boundary/Boundary.hpp"
#include "fesa/Core/Core.hpp"
#include "fesa/IO/IO.hpp"
#include "fesa/Load/Load.hpp"
#include "fesa/Math/Math.hpp"
#include "fesa/ModuleInfo.hpp"
@@ -28,449 +29,6 @@
namespace fesa {
struct KeywordLine {
std::string name;
std::map<std::string, std::string> parameters;
std::set<std::string> flags;
};
inline KeywordLine parseKeywordLine(const std::string& line) {
KeywordLine keyword;
std::vector<std::string> 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<Diagnostic> diagnostics;
bool ok() const {
return !hasError(diagnostics);
}
};
class AbaqusInputParser {
public:
ParseResult parseString(const std::string& text, const std::string& file_name = "<memory>") 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<const char*> 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<const char*> allowed_parameters,
std::initializer_list<const char*> 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<std::string> 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<std::string>& fields) {
std::size_t count = fields.size();
while (count > 0 && trim(fields[count - 1]).empty()) {
--count;
}
return count;
}
static void parseNode(const std::vector<std::string>& 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<std::string>& 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<GlobalId, 4> nodes{};
bool ok = id.has_value();
for (int i = 0; i < 4; ++i) {
auto node = parseInt64(fields[1 + static_cast<std::size_t>(i)]);
ok = ok && node.has_value();
if (node) {
nodes[static_cast<std::size_t>(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<std::string>& 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<std::string>& 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<std::string>& fields, bool generate, std::vector<GlobalId>& output,
std::vector<Diagnostic>& 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<std::string>& 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<std::string>& 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<std::string>& 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<Real>(0.0);
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) || !dofFromAbaqus(static_cast<int>(*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<int>(*first), static_cast<int>(*last), *magnitude});
}
static void parseLoad(const std::vector<std::string>& 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<int>(*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<int>(*dof), *magnitude});
}
};
inline SparsePattern buildReducedSparsePattern(const Domain& domain, const DofManager& dofs) {
SparsePattern pattern;
pattern.equation_count = dofs.freeDofCount();