#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace fesa { using Real = double; using GlobalId = std::int64_t; using LocalIndex = std::int64_t; using EquationId = std::int64_t; using SparseIndex = std::int64_t; enum class Severity { Info, Warning, Error }; struct SourceLocation { std::string file; LocalIndex line = 0; std::string keyword; }; struct Diagnostic { Severity severity = Severity::Error; std::string code; std::string message; SourceLocation source; }; inline bool hasError(const std::vector& diagnostics) { return std::any_of(diagnostics.begin(), diagnostics.end(), [](const Diagnostic& diagnostic) { return diagnostic.severity == Severity::Error; }); } inline bool containsDiagnostic(const std::vector& diagnostics, const std::string& code) { return std::any_of(diagnostics.begin(), diagnostics.end(), [&](const Diagnostic& diagnostic) { return diagnostic.code == code; }); } inline Diagnostic makeDiagnostic(Severity severity, std::string code, std::string message, std::string keyword, std::string file = "", LocalIndex line = 0) { return {severity, std::move(code), std::move(message), {std::move(file), line, std::move(keyword)}}; } inline std::string trim(std::string text) { auto is_space = [](unsigned char c) { return std::isspace(c) != 0; }; text.erase(text.begin(), std::find_if(text.begin(), text.end(), [&](unsigned char c) { return !is_space(c); })); text.erase(std::find_if(text.rbegin(), text.rend(), [&](unsigned char c) { return !is_space(c); }).base(), text.end()); return text; } inline std::string lower(std::string text) { std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) { return static_cast(std::tolower(c)); }); return text; } inline std::vector splitCsv(const std::string& line) { std::vector fields; std::string field; std::istringstream stream(line); while (std::getline(stream, field, ',')) { fields.push_back(trim(field)); } if (!line.empty() && line.back() == ',') { fields.emplace_back(); } return fields; } inline std::optional parseReal(std::string token) { token = trim(token); if (token.empty()) { return std::nullopt; } std::replace(token.begin(), token.end(), 'D', 'E'); std::replace(token.begin(), token.end(), 'd', 'e'); try { std::size_t used = 0; Real value = std::stod(token, &used); if (used != token.size()) { return std::nullopt; } return value; } catch (...) { return std::nullopt; } } inline std::optional parseInt64(const std::string& token) { std::string value_text = trim(token); if (value_text.empty()) { return std::nullopt; } try { std::size_t used = 0; long long value = std::stoll(value_text, &used); if (used != value_text.size()) { return std::nullopt; } return static_cast(value); } catch (...) { return std::nullopt; } } enum class Dof : int { UX = 0, UY = 1, UZ = 2, RX = 3, RY = 4, RZ = 5 }; inline std::array allDofs() { return {Dof::UX, Dof::UY, Dof::UZ, Dof::RX, Dof::RY, Dof::RZ}; } inline int dofIndex(Dof dof) { return static_cast(dof); } inline int abaqusDofNumber(Dof dof) { return dofIndex(dof) + 1; } inline std::optional dofFromAbaqus(int dof) { if (dof < 1 || dof > 6) { return std::nullopt; } return static_cast(dof - 1); } inline const char* dofLabel(Dof dof) { switch (dof) { case Dof::UX: return "UX"; case Dof::UY: return "UY"; case Dof::UZ: return "UZ"; case Dof::RX: return "RX"; case Dof::RY: return "RY"; case Dof::RZ: return "RZ"; } return ""; } inline std::vector displacementComponentLabels() { return {"UX", "UY", "UZ", "RX", "RY", "RZ"}; } inline std::vector reactionComponentLabels() { return {"RFX", "RFY", "RFZ", "RMX", "RMY", "RMZ"}; } struct Vec3 { Real x = 0.0; Real y = 0.0; Real z = 0.0; }; inline Vec3 operator+(const Vec3& a, const Vec3& b) { return {a.x + b.x, a.y + b.y, a.z + b.z}; } inline Vec3 operator-(const Vec3& a, const Vec3& b) { return {a.x - b.x, a.y - b.y, a.z - b.z}; } inline Vec3 operator*(Real scalar, const Vec3& value) { return {scalar * value.x, scalar * value.y, scalar * value.z}; } inline Real dot(const Vec3& a, const Vec3& b) { return a.x * b.x + a.y * b.y + a.z * b.z; } inline Vec3 cross(const Vec3& a, const Vec3& b) { return {a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x}; } inline Real norm(const Vec3& value) { return std::sqrt(dot(value, value)); } inline bool isFinite(Real value) { return std::isfinite(value); } inline bool isFinite(const Vec3& value) { return isFinite(value.x) && isFinite(value.y) && isFinite(value.z); } inline std::optional normalizedIfValid(const Vec3& value, Real tolerance = 1.0e-12) { const Real length = norm(value); if (!isFinite(length) || length <= tolerance) { return std::nullopt; } return (1.0 / length) * value; } inline Vec3 normalized(const Vec3& value) { const Real length = norm(value); if (length <= std::numeric_limits::epsilon()) { throw std::runtime_error("zero-length vector"); } return (1.0 / length) * value; } struct Node { GlobalId id = 0; Vec3 coordinates; }; enum class ElementType { MITC4 }; struct Element { GlobalId id = 0; ElementType type = ElementType::MITC4; std::array node_ids{}; std::string source_elset; }; struct NodeSet { std::string name; std::vector node_ids; }; struct ElementSet { std::string name; std::vector element_ids; }; struct Material { std::string name; Real elastic_modulus = 0.0; Real poisson_ratio = 0.0; }; struct ShellSection { std::string element_set; std::string material; Real thickness = 0.0; }; struct BoundaryCondition { std::string target; int first_dof = 0; int last_dof = 0; Real magnitude = 0.0; }; struct NodalLoad { std::string target; int dof = 0; Real magnitude = 0.0; }; struct StepDefinition { std::string name = "Step-1"; std::string analysis_type = "linear_static"; }; struct Domain { std::map nodes; std::map elements; std::map node_sets; std::map element_sets; std::map materials; std::vector shell_sections; std::vector boundary_conditions; std::vector loads; std::vector steps; static std::string key(const std::string& label) { return lower(trim(label)); } }; inline void addUnique(std::vector& values, GlobalId value) { if (std::find(values.begin(), values.end(), value) == values.end()) { values.push_back(value); } } inline std::vector generatedRange(GlobalId first, GlobalId last, GlobalId increment) { std::vector values; if (increment <= 0) { return values; } for (GlobalId value = first; value <= last; value += increment) { values.push_back(value); } return values; } 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}); } }; inline std::optional numericTarget(const std::string& target) { return parseInt64(target); } inline std::vector resolveNodeTarget(const Domain& domain, const std::string& target, std::vector* diagnostics = nullptr, const std::string& diagnostic_keyword = "node target") { if (auto node_id = numericTarget(target)) { if (domain.nodes.count(*node_id) == 0) { if (diagnostics != nullptr) { diagnostics->push_back( makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NODE", "Missing node target: " + target, diagnostic_keyword)); } return {}; } return {*node_id}; } auto set_it = domain.node_sets.find(Domain::key(target)); if (set_it == domain.node_sets.end()) { if (diagnostics != nullptr) { diagnostics->push_back( makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-NSET", "Missing node set: " + target, diagnostic_keyword)); } return {}; } return set_it->second.node_ids; } inline const ShellSection* shellSectionForElement(const Domain& domain, GlobalId element_id) { for (const ShellSection& section : domain.shell_sections) { auto set_it = domain.element_sets.find(Domain::key(section.element_set)); if (set_it == domain.element_sets.end()) { continue; } if (std::find(set_it->second.element_ids.begin(), set_it->second.element_ids.end(), element_id) != set_it->second.element_ids.end()) { return §ion; } } return nullptr; } inline std::string dofNameOrNumber(int abaqus_dof) { auto dof = dofFromAbaqus(abaqus_dof); if (dof) { return dofLabel(*dof); } return "DOF " + std::to_string(abaqus_dof); } inline bool validAbaqusDofRange(int first, int last) { return dofFromAbaqus(first).has_value() && dofFromAbaqus(last).has_value() && first <= last; } inline std::vector validateDomain(const Domain& domain) { std::vector diagnostics; if (domain.elements.empty()) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-ACTIVE-ELEMENTS", "No active elements exist in the current model", "analysis model")); } if (domain.boundary_conditions.empty()) { diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-BOUNDARY", "No boundary constraints are defined", "boundary")); } for (const auto& [set_key, set] : domain.node_sets) { (void)set_key; for (GlobalId node_id : set.node_ids) { if (domain.nodes.count(node_id) == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NSET-MISSING-NODE", "Node set " + set.name + " references missing node " + std::to_string(node_id), "nset")); } } } for (const auto& [set_key, set] : domain.element_sets) { (void)set_key; for (GlobalId element_id : set.element_ids) { if (domain.elements.count(element_id) == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELSET-MISSING-ELEMENT", "Element set " + set.name + " references missing element " + std::to_string(element_id), "elset")); } } } for (const auto& [id, element] : domain.elements) { for (GlobalId node_id : element.node_ids) { if (domain.nodes.count(node_id) == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-ELEMENT-MISSING-NODE", "Element " + std::to_string(id) + " references missing node " + std::to_string(node_id), "element")); } } const ShellSection* section = shellSectionForElement(domain, id); if (section == nullptr) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-PROPERTY", "Element " + std::to_string(id) + " has no assigned shell section", "element")); } } for (const ShellSection& section : domain.shell_sections) { if (section.thickness <= 0.0 || !std::isfinite(section.thickness)) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-NONPOSITIVE-THICKNESS", "Shell section for element set " + section.element_set + " has non-positive thickness", "shell section")); } if (domain.element_sets.count(Domain::key(section.element_set)) == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-ELSET", "Shell section references missing element set: " + section.element_set, "shell section")); } auto material_it = domain.materials.find(Domain::key(section.material)); if (material_it == domain.materials.end()) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-MISSING-MATERIAL", "Shell section references missing material: " + section.material, "shell section")); } else if (material_it->second.elastic_modulus <= 0.0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-INCOMPLETE-MATERIAL", "Material has no valid elastic constants: " + section.material, "material")); } } for (const BoundaryCondition& boundary : domain.boundary_conditions) { if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-BOUNDARY-DOF", "Boundary target " + boundary.target + " has invalid DOF range " + dofNameOrNumber(boundary.first_dof) + " to " + dofNameOrNumber(boundary.last_dof), "boundary")); } (void)resolveNodeTarget(domain, boundary.target, &diagnostics, "boundary"); } for (const NodalLoad& load : domain.loads) { if (!dofFromAbaqus(load.dof)) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-VALIDATION-CLOAD-DOF", "Load target " + load.target + " has invalid " + dofNameOrNumber(load.dof), "cload")); } (void)resolveNodeTarget(domain, load.target, &diagnostics, "cload"); } const bool any_nonzero_load = std::any_of(domain.loads.begin(), domain.loads.end(), [](const NodalLoad& load) { return std::fabs(load.magnitude) > 0.0; }); if (!any_nonzero_load) { diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-NO-NONZERO-LOAD", "No nonzero load is defined", "cload")); } std::set> constrained_dofs; for (const BoundaryCondition& boundary : domain.boundary_conditions) { if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) { continue; } for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) { if (domain.nodes.count(node_id) == 0) { continue; } for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { constrained_dofs.insert(std::make_pair(node_id, dof - 1)); } } } std::set active_connectivity_nodes; for (const auto& [element_id, element] : domain.elements) { (void)element_id; for (GlobalId node_id : element.node_ids) { if (domain.nodes.count(node_id) != 0) { active_connectivity_nodes.insert(node_id); } } } LocalIndex free_dof_count = 0; LocalIndex weak_drilling_count = 0; GlobalId weak_drilling_example = 0; for (const auto& [node_id, node] : domain.nodes) { (void)node; for (Dof dof : allDofs()) { const auto key = std::make_pair(node_id, dofIndex(dof)); if (constrained_dofs.count(key) != 0) { continue; } ++free_dof_count; if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-DOF-UNTOUCHED", "Node " + std::to_string(node_id) + " DOF " + dofLabel(dof) + " is free but is not touched by active element connectivity", "dof")); } if (!domain.elements.empty() && active_connectivity_nodes.count(node_id) != 0 && dof == Dof::RZ) { if (weak_drilling_count == 0) { weak_drilling_example = node_id; } ++weak_drilling_count; } } } if (!domain.nodes.empty() && free_dof_count == 0) { diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS", "No free DOFs exist after applying boundary constraints", "dof")); } if (weak_drilling_count > 0) { diagnostics.push_back(makeDiagnostic(Severity::Warning, "FESA-SINGULAR-WEAK-DRILLING-DOF", "Node " + std::to_string(weak_drilling_example) + " DOF RZ is free; drilling rotation is weakly stabilized in Phase 1 (" + std::to_string(weak_drilling_count) + " free drilling DOF(s))", "dof")); } return diagnostics; } struct DofAddress { GlobalId node_id = 0; Dof dof = Dof::UX; }; class DofManager { public: explicit DofManager(const Domain& domain) { for (const auto& [node_id, node] : domain.nodes) { (void)node; node_ids_.push_back(node_id); for (Dof dof : allDofs()) { const LocalIndex full_index = static_cast(all_dofs_.size()); const auto key = std::make_pair(node_id, dofIndex(dof)); all_dofs_.push_back(key); full_index_by_key_[key] = full_index; } } for (const BoundaryCondition& boundary : domain.boundary_conditions) { if (!validAbaqusDofRange(boundary.first_dof, boundary.last_dof)) { continue; } for (GlobalId node_id : resolveNodeTarget(domain, boundary.target)) { for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) { constrained_.insert(std::make_pair(node_id, dof - 1)); } } } for (const auto& key : all_dofs_) { const LocalIndex full_index = full_index_by_key_.at(key); if (constrained_.count(key) == 0) { equation_by_key_[key] = static_cast(free_full_indices_.size()); free_full_indices_.push_back(full_index); } else { equation_by_key_[key] = -1; constrained_full_indices_.push_back(full_index); } } } LocalIndex fullDofCount() const { return static_cast(all_dofs_.size()); } LocalIndex freeDofCount() const { return static_cast(free_full_indices_.size()); } LocalIndex constrainedDofCount() const { return static_cast(constrained_full_indices_.size()); } const std::vector& nodeIds() const { return node_ids_; } const std::vector& freeFullIndices() const { return free_full_indices_; } const std::vector& constrainedFullIndices() const { return constrained_full_indices_; } DofAddress fullDof(LocalIndex full_index) const { const auto& key = all_dofs_.at(static_cast(full_index)); return {key.first, static_cast(key.second)}; } LocalIndex fullIndex(GlobalId node_id, Dof dof) const { return full_index_by_key_.at(std::make_pair(node_id, dofIndex(dof))); } EquationId equation(GlobalId node_id, Dof dof) const { return equation_by_key_.at(std::make_pair(node_id, dofIndex(dof))); } bool isConstrained(GlobalId node_id, Dof dof) const { return constrained_.count(std::make_pair(node_id, dofIndex(dof))) != 0; } std::vector reduceFullVector(const std::vector& full) const { std::vector reduced; reduced.reserve(free_full_indices_.size()); for (LocalIndex full_index : free_full_indices_) { reduced.push_back(full.at(static_cast(full_index))); } return reduced; } std::vector reconstructFullVector(const std::vector& reduced) const { std::vector full(static_cast(fullDofCount()), 0.0); for (std::size_t i = 0; i < free_full_indices_.size(); ++i) { full[static_cast(free_full_indices_[i])] = reduced.at(i); } return full; } std::array elementFullDofIndices(const Element& element) const { std::array indices{}; for (LocalIndex node = 0; node < 4; ++node) { for (Dof dof : allDofs()) { const LocalIndex local = 6 * node + dofIndex(dof); indices[static_cast(local)] = fullIndex(element.node_ids[static_cast(node)], dof); } } return indices; } std::array elementEquationIds(const Element& element) const { std::array equations{}; for (LocalIndex node = 0; node < 4; ++node) { for (Dof dof : allDofs()) { const LocalIndex local = 6 * node + dofIndex(dof); equations[static_cast(local)] = equation(element.node_ids[static_cast(node)], dof); } } return equations; } private: std::vector node_ids_; std::vector> all_dofs_; std::set> constrained_; std::map, LocalIndex> full_index_by_key_; std::map, EquationId> equation_by_key_; std::vector free_full_indices_; std::vector constrained_full_indices_; }; struct SparsePatternEntry { EquationId row = 0; EquationId col = 0; }; struct SparsePattern { EquationId equation_count = 0; std::vector entries; SparseIndex nonzeroCount() const { return static_cast(entries.size()); } bool contains(EquationId row, EquationId col) const { return std::any_of(entries.begin(), entries.end(), [&](const SparsePatternEntry& entry) { return entry.row == row && entry.col == col; }); } }; inline SparsePattern buildReducedSparsePattern(const Domain& domain, const DofManager& dofs) { SparsePattern pattern; pattern.equation_count = dofs.freeDofCount(); std::set> ordered_entries; for (const auto& [element_id, element] : domain.elements) { (void)element_id; const auto equations = dofs.elementEquationIds(element); for (EquationId row : equations) { if (row < 0) { continue; } for (EquationId col : equations) { if (col < 0) { continue; } ordered_entries.insert({row, col}); } } } pattern.entries.reserve(ordered_entries.size()); for (const auto& entry : ordered_entries) { pattern.entries.push_back({entry.first, entry.second}); } return pattern; } class DenseMatrix { public: DenseMatrix() = default; DenseMatrix(LocalIndex rows, LocalIndex cols) : rows_(rows), cols_(cols), values_(static_cast(rows * cols), 0.0) {} LocalIndex rows() const { return rows_; } LocalIndex cols() const { return cols_; } Real& operator()(LocalIndex row, LocalIndex col) { return values_[static_cast(row * cols_ + col)]; } Real operator()(LocalIndex row, LocalIndex col) const { return values_[static_cast(row * cols_ + col)]; } void add(LocalIndex row, LocalIndex col, Real value) { (*this)(row, col) += value; } std::vector multiply(const std::vector& x) const { std::vector y(static_cast(rows_), 0.0); for (LocalIndex i = 0; i < rows_; ++i) { Real sum = 0.0; for (LocalIndex j = 0; j < cols_; ++j) { sum += (*this)(i, j) * x[static_cast(j)]; } y[static_cast(i)] = sum; } return y; } private: LocalIndex rows_ = 0; LocalIndex cols_ = 0; std::vector values_; }; inline std::vector recoverFullReaction(const DenseMatrix& k_full, const std::vector& u_full, const std::vector& f_full) { if (k_full.rows() != k_full.cols() || static_cast(u_full.size()) != k_full.cols() || static_cast(f_full.size()) != k_full.rows()) { throw std::runtime_error("full reaction size mismatch"); } std::vector reaction = k_full.multiply(u_full); for (std::size_t i = 0; i < reaction.size(); ++i) { reaction[i] -= f_full[i]; } return reaction; } struct SolveResult { std::vector x; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; class LinearSolver { public: virtual ~LinearSolver() = default; virtual SolveResult solve(DenseMatrix a, std::vector b) const = 0; }; class GaussianEliminationSolver final : public LinearSolver { public: SolveResult solve(DenseMatrix a, std::vector b) const override { const LocalIndex n = a.rows(); SolveResult result; if (a.rows() != a.cols() || static_cast(b.size()) != n) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SOLVER-SIZE", "Linear system size mismatch", "solver")); return result; } for (LocalIndex col = 0; col < n; ++col) { LocalIndex pivot = col; Real pivot_abs = std::fabs(a(col, col)); for (LocalIndex row = col + 1; row < n; ++row) { const Real candidate = std::fabs(a(row, col)); if (candidate > pivot_abs) { pivot_abs = candidate; pivot = row; } } if (pivot_abs < 1.0e-12) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-SOLVER", "Reduced system is singular or ill-conditioned", "solver")); return result; } if (pivot != col) { for (LocalIndex j = col; j < n; ++j) { std::swap(a(col, j), a(pivot, j)); } std::swap(b[static_cast(col)], b[static_cast(pivot)]); } const Real diag = a(col, col); for (LocalIndex row = col + 1; row < n; ++row) { const Real factor = a(row, col) / diag; a(row, col) = 0.0; for (LocalIndex j = col + 1; j < n; ++j) { a(row, j) -= factor * a(col, j); } b[static_cast(row)] -= factor * b[static_cast(col)]; } } result.x.assign(static_cast(n), 0.0); for (LocalIndex i = n; i-- > 0;) { Real sum = b[static_cast(i)]; for (LocalIndex j = i + 1; j < n; ++j) { sum -= a(i, j) * result.x[static_cast(j)]; } result.x[static_cast(i)] = sum / a(i, i); } return result; } }; struct ShapeData { std::array n{}; std::array dr{}; std::array ds{}; }; inline ShapeData shapeFunctions(Real r, Real s) { return {{ 0.25 * (1.0 - r) * (1.0 - s), 0.25 * (1.0 + r) * (1.0 - s), 0.25 * (1.0 + r) * (1.0 + s), 0.25 * (1.0 - r) * (1.0 + s), }, { -0.25 * (1.0 - s), 0.25 * (1.0 - s), 0.25 * (1.0 + s), -0.25 * (1.0 + s), }, { -0.25 * (1.0 - r), -0.25 * (1.0 + r), 0.25 * (1.0 + r), 0.25 * (1.0 - r), }}; } struct LocalBasis { Vec3 e1; Vec3 e2; Vec3 e3; }; struct MITC4NaturalPoint { Real xi = 0.0; Real eta = 0.0; }; struct MITC4TyingPoint { std::string label; MITC4NaturalPoint natural; std::array edge_node_indices{}; }; inline std::array mitc4NodeNaturalCoordinates() { return {{{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}}}; } inline std::array mitc4TyingPoints() { return {{{"A", {0.0, -1.0}, {0, 1}}, {"B", {-1.0, 0.0}, {0, 3}}, {"C", {0.0, 1.0}, {3, 2}}, {"D", {1.0, 0.0}, {1, 2}}}}; } struct MITC4DirectorFrame { Vec3 v1; Vec3 v2; Vec3 vn; }; struct MITC4MidsurfaceDerivatives { ShapeData shape; Vec3 g1; Vec3 g2; }; struct MITC4Geometry { std::array coordinates{}; Real thickness = 0.0; ShapeData center_shape; Vec3 g1_center; Vec3 g2_center; Vec3 center_normal; std::array nodal_frames{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4IntegrationBasis { ShapeData shape; Vec3 g1; Vec3 g2; Vec3 g3; Real jacobian = 0.0; LocalBasis local; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; using MITC4ElementDofVector = std::array; using MITC4StrainVector = std::array; using MITC4StrainRow = std::array; using MITC4MaterialMatrix = std::array, 6>; enum class MITC4StrainComponent { Eps11 = 0, Eps22 = 1, Eps33 = 2, Gamma23 = 3, Gamma13 = 4, Gamma12 = 5 }; inline std::size_t strainComponentIndex(MITC4StrainComponent component) { return static_cast(component); } inline std::array mitc4StrainComponentLabels() { return {"eps11", "eps22", "eps33", "gamma23", "gamma13", "gamma12"}; } struct MITC4LocalRotations { Real alpha = 0.0; Real beta = 0.0; Real gamma = 0.0; }; struct MITC4DisplacementDerivatives { ShapeData shape; Vec3 displacement; Vec3 du_dxi; Vec3 du_deta; Vec3 du_dzeta; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4StrainEvaluation { MITC4StrainVector values{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4StrainRows { std::array rows{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4MaterialMatrixEvaluation { MITC4MaterialMatrix matrix{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4StrainTransform { MITC4MaterialMatrix matrix{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4IntegrationPoint { Real xi = 0.0; Real eta = 0.0; Real zeta = 0.0; Real weight = 0.0; }; struct MITC4MaterialIntegrationSample { MITC4IntegrationPoint point; MITC4IntegrationBasis basis; MITC4StrainRows strain_rows; MITC4MaterialMatrix local_material{}; MITC4MaterialMatrix strain_transform{}; MITC4MaterialMatrix convected_material{}; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4MaterialIntegrationData { std::vector samples; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; inline Vec3 globalEX() { return {1.0, 0.0, 0.0}; } inline Vec3 globalEY() { return {0.0, 1.0, 0.0}; } inline Vec3 globalEZ() { return {0.0, 0.0, 1.0}; } inline Diagnostic mitc4Diagnostic(std::string code, std::string message) { return makeDiagnostic(Severity::Error, std::move(code), std::move(message), "mitc4", "", 0); } inline void appendDiagnostics(std::vector& target, const std::vector& source) { target.insert(target.end(), source.begin(), source.end()); } inline MITC4MidsurfaceDerivatives mitc4MidsurfaceDerivatives(const std::array& coordinates, Real xi, Real eta) { MITC4MidsurfaceDerivatives result; result.shape = shapeFunctions(xi, eta); for (std::size_t i = 0; i < 4; ++i) { result.g1 = result.g1 + result.shape.dr[i] * coordinates[i]; result.g2 = result.g2 + result.shape.ds[i] * coordinates[i]; } return result; } inline std::optional firstNormalizedCross(const std::array& axes, const Vec3& vector, Real tolerance) { for (const Vec3& axis : axes) { auto candidate = normalizedIfValid(cross(axis, vector), tolerance); if (candidate) { return candidate; } } return std::nullopt; } inline std::optional buildMITC4DirectorFrame(const Vec3& normal, Real tolerance) { auto v1 = normalizedIfValid(cross(globalEY(), normal), tolerance); if (!v1) { v1 = firstNormalizedCross({globalEZ(), globalEX(), globalEY()}, normal, tolerance); } if (!v1) { return std::nullopt; } auto v2 = normalizedIfValid(cross(normal, *v1), tolerance); if (!v2) { return std::nullopt; } return MITC4DirectorFrame{*v1, *v2, normal}; } inline MITC4Geometry buildMITC4Geometry(const std::array& coordinates, Real thickness, Real tolerance = 1.0e-12) { MITC4Geometry geometry; geometry.coordinates = coordinates; geometry.thickness = thickness; if (!isFinite(thickness) || thickness <= tolerance) { geometry.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-THICKNESS", "MITC4 shell thickness must be positive and finite")); } for (const Vec3& coordinate : coordinates) { if (!isFinite(coordinate)) { geometry.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-COORDINATE", "MITC4 element coordinates must be finite")); break; } } const auto center = mitc4MidsurfaceDerivatives(coordinates, 0.0, 0.0); geometry.center_shape = center.shape; geometry.g1_center = center.g1; geometry.g2_center = center.g2; const auto normal = normalizedIfValid(cross(center.g1, center.g2), tolerance); if (!normal) { geometry.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-NORMAL", "MITC4 element center normal is near zero")); return geometry; } geometry.center_normal = *normal; const auto frame = buildMITC4DirectorFrame(*normal, tolerance); if (!frame) { geometry.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-BASIS", "MITC4 nodal director basis could not be constructed")); return geometry; } geometry.nodal_frames.fill(*frame); return geometry; } inline MITC4IntegrationBasis computeMITC4IntegrationBasis(const MITC4Geometry& geometry, Real xi, Real eta, Real zeta, Real tolerance = 1.0e-12) { MITC4IntegrationBasis result; result.diagnostics = geometry.diagnostics; result.shape = shapeFunctions(xi, eta); for (std::size_t i = 0; i < 4; ++i) { const Vec3& coordinate = geometry.coordinates[i]; const Vec3& normal = geometry.nodal_frames[i].vn; result.g1 = result.g1 + result.shape.dr[i] * coordinate + (0.5 * zeta * geometry.thickness * result.shape.dr[i]) * normal; result.g2 = result.g2 + result.shape.ds[i] * coordinate + (0.5 * zeta * geometry.thickness * result.shape.ds[i]) * normal; result.g3 = result.g3 + (0.5 * geometry.thickness * result.shape.n[i]) * normal; } result.jacobian = dot(cross(result.g1, result.g2), result.g3); if (!isFinite(result.jacobian) || std::fabs(result.jacobian) <= tolerance) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-JACOBIAN", "MITC4 element Jacobian is near zero")); } const auto e3 = normalizedIfValid(result.g3, tolerance); if (!e3) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-BASIS", "MITC4 integration basis normal is near zero")); return result; } auto e1 = normalizedIfValid(cross(result.g2, *e3), tolerance); if (!e1) { e1 = firstNormalizedCross({globalEY(), globalEZ(), globalEX()}, *e3, tolerance); } if (!e1) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-BASIS", "MITC4 integration basis tangent could not be constructed")); return result; } const auto e2 = normalizedIfValid(cross(*e3, *e1), tolerance); if (!e2) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-BASIS", "MITC4 integration basis is not right-handed")); return result; } result.local = {*e1, *e2, *e3}; return result; } inline Vec3 mitc4NodalTranslation(const MITC4ElementDofVector& values, std::size_t node) { const std::size_t base = 6 * node; return {values[base + 0], values[base + 1], values[base + 2]}; } inline Vec3 mitc4NodalRotation(const MITC4ElementDofVector& values, std::size_t node) { const std::size_t base = 6 * node; return {values[base + 3], values[base + 4], values[base + 5]}; } inline MITC4LocalRotations mitc4LocalRotations(const MITC4DirectorFrame& frame, const Vec3& global_rotation) { return {dot(global_rotation, frame.v1), dot(global_rotation, frame.v2), dot(global_rotation, frame.vn)}; } inline Vec3 mitc4DirectorIncrement(const MITC4DirectorFrame& frame, const Vec3& global_rotation) { const MITC4LocalRotations rotations = mitc4LocalRotations(frame, global_rotation); return (-rotations.alpha) * frame.v2 + rotations.beta * frame.v1; } inline MITC4DisplacementDerivatives mitc4DisplacementDerivatives(const MITC4Geometry& geometry, const MITC4ElementDofVector& values, Real xi, Real eta, Real zeta) { MITC4DisplacementDerivatives result; result.diagnostics = geometry.diagnostics; result.shape = shapeFunctions(xi, eta); for (std::size_t node = 0; node < 4; ++node) { const Vec3 translation = mitc4NodalTranslation(values, node); const Vec3 rotation = mitc4NodalRotation(values, node); const Vec3 q = mitc4DirectorIncrement(geometry.nodal_frames[node], rotation); const Real n = result.shape.n[node]; const Real dn_dxi = result.shape.dr[node]; const Real dn_deta = result.shape.ds[node]; result.displacement = result.displacement + n * translation + (0.5 * zeta * geometry.thickness * n) * q; result.du_dxi = result.du_dxi + dn_dxi * translation + (0.5 * zeta * geometry.thickness * dn_dxi) * q; result.du_deta = result.du_deta + dn_deta * translation + (0.5 * zeta * geometry.thickness * dn_deta) * q; result.du_dzeta = result.du_dzeta + (0.5 * geometry.thickness * n) * q; } return result; } inline void assignMITC4CovariantStrain(MITC4StrainVector& values, const MITC4IntegrationBasis& basis, const MITC4DisplacementDerivatives& derivatives) { values[strainComponentIndex(MITC4StrainComponent::Eps11)] = dot(derivatives.du_dxi, basis.g1); values[strainComponentIndex(MITC4StrainComponent::Eps22)] = dot(derivatives.du_deta, basis.g2); values[strainComponentIndex(MITC4StrainComponent::Eps33)] = 0.0; values[strainComponentIndex(MITC4StrainComponent::Gamma23)] = dot(derivatives.du_deta, basis.g3) + dot(derivatives.du_dzeta, basis.g2); values[strainComponentIndex(MITC4StrainComponent::Gamma13)] = dot(derivatives.du_dxi, basis.g3) + dot(derivatives.du_dzeta, basis.g1); values[strainComponentIndex(MITC4StrainComponent::Gamma12)] = dot(derivatives.du_dxi, basis.g2) + dot(derivatives.du_deta, basis.g1); } inline MITC4StrainEvaluation mitc4DirectCovariantStrain(const MITC4Geometry& geometry, const MITC4ElementDofVector& values, Real xi, Real eta, Real zeta) { MITC4StrainEvaluation result; const auto basis = computeMITC4IntegrationBasis(geometry, xi, eta, zeta); appendDiagnostics(result.diagnostics, basis.diagnostics); const auto derivatives = mitc4DisplacementDerivatives(geometry, values, xi, eta, zeta); appendDiagnostics(result.diagnostics, derivatives.diagnostics); if (hasError(result.diagnostics)) { return result; } assignMITC4CovariantStrain(result.values, basis, derivatives); return result; } inline MITC4StrainRows mitc4DirectCovariantStrainRows(const MITC4Geometry& geometry, Real xi, Real eta, Real zeta) { MITC4StrainRows result; const auto basis = computeMITC4IntegrationBasis(geometry, xi, eta, zeta); appendDiagnostics(result.diagnostics, basis.diagnostics); if (hasError(result.diagnostics)) { return result; } for (std::size_t dof = 0; dof < 24; ++dof) { MITC4ElementDofVector unit{}; unit.fill(0.0); unit[dof] = 1.0; const auto derivatives = mitc4DisplacementDerivatives(geometry, unit, xi, eta, zeta); appendDiagnostics(result.diagnostics, derivatives.diagnostics); if (hasError(result.diagnostics)) { return result; } MITC4StrainVector values{}; assignMITC4CovariantStrain(values, basis, derivatives); for (std::size_t component = 0; component < 6; ++component) { result.rows[component][dof] = values[component]; } } return result; } inline MITC4StrainEvaluation evaluateMITC4StrainRows(const MITC4StrainRows& rows, const MITC4ElementDofVector& values) { MITC4StrainEvaluation result; result.diagnostics = rows.diagnostics; if (hasError(result.diagnostics)) { return result; } for (std::size_t component = 0; component < 6; ++component) { for (std::size_t dof = 0; dof < 24; ++dof) { result.values[component] += rows.rows[component][dof] * values[dof]; } } return result; } inline MITC4StrainRows mitc4TiedCovariantStrainRows(const MITC4Geometry& geometry, Real xi, Real eta, Real zeta) { MITC4StrainRows result = mitc4DirectCovariantStrainRows(geometry, xi, eta, zeta); const auto direct_a = mitc4DirectCovariantStrainRows(geometry, 0.0, -1.0, zeta); const auto direct_b = mitc4DirectCovariantStrainRows(geometry, -1.0, 0.0, zeta); const auto direct_c = mitc4DirectCovariantStrainRows(geometry, 0.0, 1.0, zeta); const auto direct_d = mitc4DirectCovariantStrainRows(geometry, 1.0, 0.0, zeta); appendDiagnostics(result.diagnostics, direct_a.diagnostics); appendDiagnostics(result.diagnostics, direct_b.diagnostics); appendDiagnostics(result.diagnostics, direct_c.diagnostics); appendDiagnostics(result.diagnostics, direct_d.diagnostics); if (hasError(result.diagnostics)) { return result; } const std::size_t gamma23 = strainComponentIndex(MITC4StrainComponent::Gamma23); const std::size_t gamma13 = strainComponentIndex(MITC4StrainComponent::Gamma13); for (std::size_t dof = 0; dof < 24; ++dof) { result.rows[gamma13][dof] = 0.5 * (1.0 - eta) * direct_a.rows[gamma13][dof] + 0.5 * (1.0 + eta) * direct_c.rows[gamma13][dof]; result.rows[gamma23][dof] = 0.5 * (1.0 - xi) * direct_b.rows[gamma23][dof] + 0.5 * (1.0 + xi) * direct_d.rows[gamma23][dof]; } return result; } inline std::array mitc4GaussQuadrature2x2x2() { const Real gauss = 1.0 / std::sqrt(3.0); const std::array points = {-gauss, gauss}; std::array integration_points{}; std::size_t index = 0; for (Real xi : points) { for (Real eta : points) { for (Real zeta : points) { integration_points[index++] = {xi, eta, zeta, 1.0}; } } } return integration_points; } inline MITC4MaterialMatrixEvaluation mitc4PlaneStressMaterialMatrix(Real elastic_modulus, Real poisson_ratio, Real shear_correction = 5.0 / 6.0, Real tolerance = 1.0e-12) { MITC4MaterialMatrixEvaluation result; if (!isFinite(elastic_modulus) || elastic_modulus <= tolerance) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-MATERIAL", "MITC4 elastic modulus must be positive and finite")); } if (!isFinite(poisson_ratio) || poisson_ratio <= -1.0 || poisson_ratio >= 0.5) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-POISSON", "MITC4 isotropic Poisson ratio must satisfy -1 < nu < 0.5")); } if (!isFinite(shear_correction) || shear_correction <= tolerance) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SHEAR-CORRECTION", "MITC4 shear correction factor must be positive and finite")); } if (hasError(result.diagnostics)) { return result; } const Real scale = elastic_modulus / (1.0 - poisson_ratio * poisson_ratio); const Real shear_modulus = elastic_modulus / (2.0 * (1.0 + poisson_ratio)); const std::size_t eps11 = strainComponentIndex(MITC4StrainComponent::Eps11); const std::size_t eps22 = strainComponentIndex(MITC4StrainComponent::Eps22); const std::size_t gamma23 = strainComponentIndex(MITC4StrainComponent::Gamma23); const std::size_t gamma13 = strainComponentIndex(MITC4StrainComponent::Gamma13); const std::size_t gamma12 = strainComponentIndex(MITC4StrainComponent::Gamma12); result.matrix[eps11][eps11] = scale; result.matrix[eps11][eps22] = poisson_ratio * scale; result.matrix[eps22][eps11] = poisson_ratio * scale; result.matrix[eps22][eps22] = scale; result.matrix[gamma23][gamma23] = shear_correction * shear_modulus; result.matrix[gamma13][gamma13] = shear_correction * shear_modulus; result.matrix[gamma12][gamma12] = shear_modulus; return result; } inline MITC4StrainVector multiplyMITC4MaterialMatrix(const MITC4MaterialMatrix& matrix, const MITC4StrainVector& vector) { MITC4StrainVector result{}; for (std::size_t row = 0; row < 6; ++row) { for (std::size_t col = 0; col < 6; ++col) { result[row] += matrix[row][col] * vector[col]; } } return result; } inline Real dotMITC4Vector(const MITC4StrainVector& a, const MITC4StrainVector& b) { Real result = 0.0; for (std::size_t i = 0; i < 6; ++i) { result += a[i] * b[i]; } return result; } inline MITC4MaterialMatrix mitc4TransformMaterialMatrix(const MITC4MaterialMatrix& local_material, const MITC4MaterialMatrix& covariant_to_local) { MITC4MaterialMatrix result{}; for (std::size_t i = 0; i < 6; ++i) { for (std::size_t j = 0; j < 6; ++j) { Real value = 0.0; for (std::size_t a = 0; a < 6; ++a) { for (std::size_t b = 0; b < 6; ++b) { value += covariant_to_local[a][i] * local_material[a][b] * covariant_to_local[b][j]; } } result[i][j] = value; } } return result; } inline std::array, 3> mitc4TensorFromEngineeringComponent(std::size_t component) { std::array, 3> tensor{}; switch (static_cast(component)) { case MITC4StrainComponent::Eps11: tensor[0][0] = 1.0; break; case MITC4StrainComponent::Eps22: tensor[1][1] = 1.0; break; case MITC4StrainComponent::Eps33: tensor[2][2] = 1.0; break; case MITC4StrainComponent::Gamma23: tensor[1][2] = 0.5; tensor[2][1] = 0.5; break; case MITC4StrainComponent::Gamma13: tensor[0][2] = 0.5; tensor[2][0] = 0.5; break; case MITC4StrainComponent::Gamma12: tensor[0][1] = 0.5; tensor[1][0] = 0.5; break; } return tensor; } inline MITC4StrainVector mitc4EngineeringVectorFromTensor(const std::array, 3>& tensor) { MITC4StrainVector vector{}; vector[strainComponentIndex(MITC4StrainComponent::Eps11)] = tensor[0][0]; vector[strainComponentIndex(MITC4StrainComponent::Eps22)] = tensor[1][1]; vector[strainComponentIndex(MITC4StrainComponent::Eps33)] = tensor[2][2]; vector[strainComponentIndex(MITC4StrainComponent::Gamma23)] = 2.0 * tensor[1][2]; vector[strainComponentIndex(MITC4StrainComponent::Gamma13)] = 2.0 * tensor[0][2]; vector[strainComponentIndex(MITC4StrainComponent::Gamma12)] = 2.0 * tensor[0][1]; return vector; } inline MITC4StrainTransform mitc4CovariantToLocalStrainTransform(const MITC4IntegrationBasis& basis, Real tolerance = 1.0e-12) { MITC4StrainTransform result; result.diagnostics = basis.diagnostics; const Real jacobian = dot(cross(basis.g1, basis.g2), basis.g3); if (!isFinite(jacobian) || std::fabs(jacobian) <= tolerance) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-SINGULAR-JACOBIAN", "MITC4 material transform Jacobian is near zero")); return result; } if (hasError(result.diagnostics)) { return result; } const std::array contravariant = { (1.0 / jacobian) * cross(basis.g2, basis.g3), (1.0 / jacobian) * cross(basis.g3, basis.g1), (1.0 / jacobian) * cross(basis.g1, basis.g2)}; const std::array local = {basis.local.e1, basis.local.e2, basis.local.e3}; std::array, 3> direction_cosines{}; for (std::size_t local_axis = 0; local_axis < 3; ++local_axis) { for (std::size_t convected_axis = 0; convected_axis < 3; ++convected_axis) { direction_cosines[local_axis][convected_axis] = dot(local[local_axis], contravariant[convected_axis]); } } for (std::size_t column = 0; column < 6; ++column) { const auto covariant_tensor = mitc4TensorFromEngineeringComponent(column); std::array, 3> local_tensor{}; for (std::size_t a = 0; a < 3; ++a) { for (std::size_t b = 0; b < 3; ++b) { for (std::size_t i = 0; i < 3; ++i) { for (std::size_t j = 0; j < 3; ++j) { local_tensor[a][b] += direction_cosines[a][i] * direction_cosines[b][j] * covariant_tensor[i][j]; } } } } const auto local_vector = mitc4EngineeringVectorFromTensor(local_tensor); for (std::size_t row = 0; row < 6; ++row) { result.matrix[row][column] = local_vector[row]; } } return result; } inline MITC4MaterialIntegrationData mitc4BuildMaterialIntegrationData(const MITC4Geometry& geometry, Real elastic_modulus, Real poisson_ratio, Real shear_correction = 5.0 / 6.0) { MITC4MaterialIntegrationData data; const auto material = mitc4PlaneStressMaterialMatrix(elastic_modulus, poisson_ratio, shear_correction); appendDiagnostics(data.diagnostics, material.diagnostics); if (hasError(data.diagnostics)) { return data; } for (const MITC4IntegrationPoint& point : mitc4GaussQuadrature2x2x2()) { MITC4MaterialIntegrationSample sample; sample.point = point; sample.local_material = material.matrix; sample.basis = computeMITC4IntegrationBasis(geometry, point.xi, point.eta, point.zeta); appendDiagnostics(sample.diagnostics, sample.basis.diagnostics); const auto transform = mitc4CovariantToLocalStrainTransform(sample.basis); sample.strain_transform = transform.matrix; appendDiagnostics(sample.diagnostics, transform.diagnostics); sample.strain_rows = mitc4TiedCovariantStrainRows(geometry, point.xi, point.eta, point.zeta); appendDiagnostics(sample.diagnostics, sample.strain_rows.diagnostics); if (!hasError(sample.diagnostics)) { sample.convected_material = mitc4TransformMaterialMatrix(sample.local_material, sample.strain_transform); } appendDiagnostics(data.diagnostics, sample.diagnostics); data.samples.push_back(sample); } return data; } inline LocalBasis computeLocalBasis(const std::array& coordinates) { const MITC4Geometry geometry = buildMITC4Geometry(coordinates, 1.0); if (!geometry.ok()) { throw std::runtime_error("invalid MITC4 geometry"); } const MITC4DirectorFrame& frame = geometry.nodal_frames[0]; return {frame.v1, frame.v2, frame.vn}; } struct ElementStiffnessOptions { Real drilling_stiffness_scale = 1.0e-3; }; struct MITC4DrillingStabilizationResult { DenseMatrix local_with_drilling; Real reference_diagonal = 0.0; Real drilling_stiffness = 0.0; Real drilling_stiffness_scale = 0.0; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct MITC4ElementStiffnessResult { DenseMatrix local_without_drilling; DenseMatrix local_with_drilling; DenseMatrix global; std::size_t integration_point_count = 0; Real drilling_reference_diagonal = 0.0; Real drilling_stiffness = 0.0; Real drilling_stiffness_scale = 0.0; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; inline DenseMatrix mitc4LocalDofTransform(const MITC4Geometry& geometry) { DenseMatrix transform(24, 24); for (LocalIndex node = 0; node < 4; ++node) { const LocalIndex base = 6 * node; const auto& frame = geometry.nodal_frames[static_cast(node)]; const std::array axes = {frame.v1, frame.v2, frame.vn}; for (LocalIndex local_axis = 0; local_axis < 3; ++local_axis) { const Vec3 axis = axes[static_cast(local_axis)]; transform(base + local_axis, base + 0) = axis.x; transform(base + local_axis, base + 1) = axis.y; transform(base + local_axis, base + 2) = axis.z; transform(base + 3 + local_axis, base + 3) = axis.x; transform(base + 3 + local_axis, base + 4) = axis.y; transform(base + 3 + local_axis, base + 5) = axis.z; } } return transform; } inline MITC4StrainRows mitc4TransformStrainRowsToLocalDofs(const MITC4StrainRows& global_rows, const DenseMatrix& local_dof_transform) { MITC4StrainRows local_rows; local_rows.diagnostics = global_rows.diagnostics; if (hasError(local_rows.diagnostics)) { return local_rows; } for (std::size_t component = 0; component < 6; ++component) { for (LocalIndex local_dof = 0; local_dof < 24; ++local_dof) { Real value = 0.0; for (LocalIndex global_dof = 0; global_dof < 24; ++global_dof) { value += global_rows.rows[component][static_cast(global_dof)] * local_dof_transform(local_dof, global_dof); } local_rows.rows[component][static_cast(local_dof)] = value; } } return local_rows; } inline void accumulateMITC4BtDB(DenseMatrix& stiffness, const MITC4StrainRows& rows, const MITC4MaterialMatrix& material, Real factor) { for (LocalIndex i = 0; i < 24; ++i) { for (LocalIndex j = 0; j < 24; ++j) { Real value = 0.0; for (std::size_t a = 0; a < 6; ++a) { for (std::size_t b = 0; b < 6; ++b) { value += rows.rows[a][static_cast(i)] * material[a][b] * rows.rows[b][static_cast(j)]; } } stiffness.add(i, j, value * factor); } } } inline Real mitc4MinimumPositivePhysicalLocalDiagonal(const DenseMatrix& local_without_drilling, Real tolerance = 1.0e-12) { Real minimum = std::numeric_limits::infinity(); for (LocalIndex node = 0; node < 4; ++node) { for (LocalIndex local_dof = 0; local_dof < 5; ++local_dof) { const Real diagonal = local_without_drilling(6 * node + local_dof, 6 * node + local_dof); if (isFinite(diagonal) && diagonal > tolerance) { minimum = std::min(minimum, diagonal); } } } return minimum; } inline MITC4DrillingStabilizationResult mitc4ApplyDrillingStabilization(const DenseMatrix& local_without_drilling, Real drilling_stiffness_scale, Real tolerance = 1.0e-12) { MITC4DrillingStabilizationResult result; result.local_with_drilling = local_without_drilling; result.drilling_stiffness_scale = drilling_stiffness_scale; if (!isFinite(drilling_stiffness_scale) || drilling_stiffness_scale < 0.0) { result.diagnostics.push_back( mitc4Diagnostic("FESA-MITC4-DRILLING-SCALE", "MITC4 drilling stiffness scale must be non-negative and finite")); return result; } result.reference_diagonal = mitc4MinimumPositivePhysicalLocalDiagonal(local_without_drilling, tolerance); if (!isFinite(result.reference_diagonal)) { result.diagnostics.push_back(mitc4Diagnostic("FESA-MITC4-DRILLING-REFERENCE", "MITC4 drilling stiffness reference diagonal is not positive")); return result; } result.drilling_stiffness = drilling_stiffness_scale * result.reference_diagonal; for (LocalIndex node = 0; node < 4; ++node) { const LocalIndex gamma = 6 * node + 5; result.local_with_drilling.add(gamma, gamma, result.drilling_stiffness); } return result; } inline DenseMatrix mitc4TransformLocalStiffnessToGlobal(const DenseMatrix& local_stiffness, const DenseMatrix& local_dof_transform) { DenseMatrix global(24, 24); for (LocalIndex i = 0; i < 24; ++i) { for (LocalIndex j = 0; j < 24; ++j) { Real value = 0.0; for (LocalIndex a = 0; a < 24; ++a) { for (LocalIndex b = 0; b < 24; ++b) { value += local_dof_transform(a, i) * local_stiffness(a, b) * local_dof_transform(b, j); } } global(i, j) = value; } } return global; } inline MITC4ElementStiffnessResult mitc4ElementStiffness(const std::array& coordinates, Real elastic_modulus, Real poisson_ratio, Real thickness, ElementStiffnessOptions options = {}) { MITC4ElementStiffnessResult result; result.local_without_drilling = DenseMatrix(24, 24); result.local_with_drilling = DenseMatrix(24, 24); result.global = DenseMatrix(24, 24); result.drilling_stiffness_scale = options.drilling_stiffness_scale; const MITC4Geometry geometry = buildMITC4Geometry(coordinates, thickness); appendDiagnostics(result.diagnostics, geometry.diagnostics); if (hasError(result.diagnostics)) { return result; } const MITC4MaterialIntegrationData integration = mitc4BuildMaterialIntegrationData(geometry, elastic_modulus, poisson_ratio); appendDiagnostics(result.diagnostics, integration.diagnostics); if (hasError(result.diagnostics)) { return result; } result.integration_point_count = integration.samples.size(); const DenseMatrix local_dof_transform = mitc4LocalDofTransform(geometry); for (const MITC4MaterialIntegrationSample& sample : integration.samples) { const MITC4StrainRows local_rows = mitc4TransformStrainRowsToLocalDofs(sample.strain_rows, local_dof_transform); appendDiagnostics(result.diagnostics, local_rows.diagnostics); if (hasError(result.diagnostics)) { return result; } const Real factor = std::fabs(sample.basis.jacobian) * sample.point.weight; accumulateMITC4BtDB(result.local_without_drilling, local_rows, sample.convected_material, factor); } const auto drilling = mitc4ApplyDrillingStabilization(result.local_without_drilling, options.drilling_stiffness_scale); appendDiagnostics(result.diagnostics, drilling.diagnostics); result.local_with_drilling = drilling.local_with_drilling; result.drilling_reference_diagonal = drilling.reference_diagonal; result.drilling_stiffness = drilling.drilling_stiffness; result.drilling_stiffness_scale = drilling.drilling_stiffness_scale; if (hasError(result.diagnostics)) { return result; } result.global = mitc4TransformLocalStiffnessToGlobal(result.local_with_drilling, local_dof_transform); return result; } inline std::vector mitc4ElementInternalForce(const MITC4ElementStiffnessResult& stiffness, const std::vector& element_displacement) { if (stiffness.global.rows() != static_cast(element_displacement.size())) { throw std::runtime_error("MITC4 internal force size mismatch"); } return stiffness.global.multiply(element_displacement); } class MITC4ElementKernel { public: DenseMatrix stiffness(const std::array& coordinates, Real elastic_modulus, Real poisson_ratio, Real thickness, ElementStiffnessOptions options = {}) const { const auto result = mitc4ElementStiffness(coordinates, elastic_modulus, poisson_ratio, thickness, options); if (!result.ok()) { throw std::runtime_error(result.diagnostics.empty() ? "invalid MITC4 stiffness" : result.diagnostics.front().message); } return result.global; } }; struct AssemblyResult { DenseMatrix k_full; std::vector f_full; SparsePattern reduced_pattern; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; struct ReducedSystem { DenseMatrix k; std::vector f; std::vector free_full_indices; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; inline AssemblyResult assembleSystem(const Domain& domain, const DofManager& dofs, ElementStiffnessOptions options = {}) { AssemblyResult result; result.k_full = DenseMatrix(dofs.fullDofCount(), dofs.fullDofCount()); result.f_full = std::vector(static_cast(dofs.fullDofCount()), 0.0); result.reduced_pattern = buildReducedSparsePattern(domain, dofs); if (dofs.freeDofCount() > 0 && result.reduced_pattern.nonzeroCount() == 0) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-SPARSE-PATTERN", "Reduced sparse pattern has no stiffness entries", "assembly")); } for (const auto& [element_id, element] : domain.elements) { const ShellSection* section = shellSectionForElement(domain, element_id); if (section == nullptr) { result.diagnostics.push_back({Severity::Error, "FESA-ASSEMBLY-MISSING-PROPERTY", "Element has no shell section", {}}); continue; } const auto material_it = domain.materials.find(Domain::key(section->material)); if (material_it == domain.materials.end()) { result.diagnostics.push_back({Severity::Error, "FESA-ASSEMBLY-MISSING-MATERIAL", "Element material is missing", {}}); continue; } std::array coordinates{}; for (std::size_t i = 0; i < 4; ++i) { coordinates[i] = domain.nodes.at(element.node_ids[i]).coordinates; } const auto stiffness = mitc4ElementStiffness(coordinates, material_it->second.elastic_modulus, material_it->second.poisson_ratio, section->thickness, options); result.diagnostics.insert(result.diagnostics.end(), stiffness.diagnostics.begin(), stiffness.diagnostics.end()); if (!stiffness.ok()) { continue; } const auto element_full_indices = dofs.elementFullDofIndices(element); for (LocalIndex a = 0; a < 24; ++a) { const LocalIndex ia = element_full_indices[static_cast(a)]; for (LocalIndex b = 0; b < 24; ++b) { const LocalIndex ib = element_full_indices[static_cast(b)]; result.k_full.add(ia, ib, stiffness.global(a, b)); } } } for (const NodalLoad& load : domain.loads) { const auto dof = dofFromAbaqus(load.dof); if (!dof) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ASSEMBLY-LOAD-DOF", "Nodal load references an invalid DOF", "cload")); continue; } for (GlobalId node_id : resolveNodeTarget(domain, load.target, &result.diagnostics)) { result.f_full[static_cast(dofs.fullIndex(node_id, *dof))] += load.magnitude; } } return result; } inline ReducedSystem projectToReducedSystem(const AssemblyResult& assembly, const DofManager& dofs) { ReducedSystem result; result.k = DenseMatrix(dofs.freeDofCount(), dofs.freeDofCount()); result.f = std::vector(static_cast(dofs.freeDofCount()), 0.0); result.free_full_indices = dofs.freeFullIndices(); if (assembly.k_full.rows() != assembly.k_full.cols() || assembly.k_full.rows() != dofs.fullDofCount() || static_cast(assembly.f_full.size()) != dofs.fullDofCount()) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-ASSEMBLY-SIZE", "Full-space stiffness/load sizes do not match DofManager", "assembly")); return result; } if (dofs.freeDofCount() == 0) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS", "No free DOFs exist after applying constraints", "dof")); return result; } if (assembly.reduced_pattern.equation_count != dofs.freeDofCount() || assembly.reduced_pattern.nonzeroCount() == 0) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-SPARSE-PATTERN", "Reduced sparse pattern is empty or inconsistent with free DOFs", "assembly")); return result; } for (LocalIndex i = 0; i < dofs.freeDofCount(); ++i) { const LocalIndex full_i = dofs.freeFullIndices()[static_cast(i)]; result.f[static_cast(i)] = assembly.f_full[static_cast(full_i)]; for (LocalIndex j = 0; j < dofs.freeDofCount(); ++j) { const LocalIndex full_j = dofs.freeFullIndices()[static_cast(j)]; result.k(i, j) = assembly.k_full(full_i, full_j); } } return result; } struct FieldOutput { std::string name; std::string position = "NODAL"; std::string entity_type = "node"; std::string basis = "GLOBAL"; std::string description; std::vector entity_ids; std::vector component_labels; std::vector> values; }; struct ResultFrame { LocalIndex frame_id = 0; LocalIndex increment = 1; LocalIndex iteration = 0; Real step_time = 1.0; Real total_time = 1.0; bool converged = true; std::string description = "Phase 1 linear static frame"; std::map field_outputs; }; struct ResultStep { std::string name; std::vector frames; }; struct ResultFile { std::string schema_name = "FESA_RESULTS"; LocalIndex schema_version = 1; std::string solver_name = "FESA"; std::string dof_convention = "UX,UY,UZ,RX,RY,RZ"; std::string sign_convention = "Abaqus-compatible"; std::string precision = "double"; std::string index_type = "int64"; std::vector node_ids; std::vector coordinates; std::vector element_ids; std::vector> connectivity; std::vector steps; }; class InMemoryResultsWriter { public: void writeLinearStatic(const Domain& domain, const DofManager& dofs, const std::vector& u_full, const std::vector& rf_full) { result_ = ResultFile{}; for (const auto& [node_id, node] : domain.nodes) { result_.node_ids.push_back(node_id); result_.coordinates.push_back(node.coordinates); } for (const auto& [element_id, element] : domain.elements) { result_.element_ids.push_back(element_id); result_.connectivity.push_back(element.node_ids); } ResultStep step; step.name = domain.steps.empty() ? "Step-1" : domain.steps.front().name; ResultFrame frame; frame.frame_id = 0; frame.field_outputs["U"] = buildNodalField("U", displacementComponentLabels(), "Nodal displacement and rotation", domain, dofs, u_full); frame.field_outputs["RF"] = buildNodalField("RF", reactionComponentLabels(), "Nodal reaction force and moment", domain, dofs, rf_full); step.frames.push_back(frame); result_.steps.push_back(step); } const ResultFile& result() const { return result_; } private: static FieldOutput buildNodalField(const std::string& name, const std::vector& labels, const std::string& description, const Domain& domain, const DofManager& dofs, const std::vector& full_values) { FieldOutput field; field.name = name; field.position = "NODAL"; field.entity_type = "node"; field.basis = "GLOBAL"; field.description = description; field.component_labels = labels; for (const auto& [node_id, node] : domain.nodes) { (void)node; field.entity_ids.push_back(node_id); std::array values{}; for (Dof dof : allDofs()) { values[static_cast(dofIndex(dof))] = full_values[static_cast(dofs.fullIndex(node_id, dof))]; } field.values.push_back(values); } return field; } ResultFile result_; }; struct AnalysisState { std::vector u_full; std::vector f_external_full; std::vector f_internal_full; std::vector reaction_full; bool converged = false; }; struct AnalysisResult { AnalysisState state; ResultFile result_file; std::vector diagnostics; bool ok() const { return !hasError(diagnostics); } }; class Analysis { public: virtual ~Analysis() = default; AnalysisResult run(const Domain& domain) const { AnalysisResult result; initialize(domain, result); if (hasError(result.diagnostics)) { return result; } solve(domain, result); return result; } protected: virtual void initialize(const Domain& domain, AnalysisResult& result) const { auto diagnostics = validateDomain(domain); result.diagnostics.insert(result.diagnostics.end(), diagnostics.begin(), diagnostics.end()); } virtual void solve(const Domain& domain, AnalysisResult& result) const = 0; }; class LinearStaticAnalysis final : public Analysis { public: explicit LinearStaticAnalysis(const LinearSolver* solver = nullptr) : solver_(solver) {} protected: void solve(const Domain& domain, AnalysisResult& result) const override { DofManager dofs(domain); if (dofs.freeDofCount() == 0) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SINGULAR-NO-FREE-DOFS", "No free DOFs exist after applying constraints", "dof")); return; } AssemblyResult assembly = assembleSystem(domain, dofs); result.diagnostics.insert(result.diagnostics.end(), assembly.diagnostics.begin(), assembly.diagnostics.end()); if (hasError(result.diagnostics)) { return; } const auto reduced = projectToReducedSystem(assembly, dofs); result.diagnostics.insert(result.diagnostics.end(), reduced.diagnostics.begin(), reduced.diagnostics.end()); if (hasError(result.diagnostics)) { return; } const LinearSolver& active_solver = solver_ == nullptr ? defaultSolver() : *solver_; SolveResult solved = active_solver.solve(reduced.k, reduced.f); result.diagnostics.insert(result.diagnostics.end(), solved.diagnostics.begin(), solved.diagnostics.end()); if (!solved.ok()) { return; } if (static_cast(solved.x.size()) != dofs.freeDofCount()) { result.diagnostics.push_back(makeDiagnostic(Severity::Error, "FESA-SOLVER-SIZE", "Linear solver returned a vector with the wrong size", "solver")); return; } result.state.u_full = dofs.reconstructFullVector(solved.x); result.state.f_external_full = assembly.f_full; result.state.f_internal_full = assembly.k_full.multiply(result.state.u_full); result.state.reaction_full = recoverFullReaction(assembly.k_full, result.state.u_full, result.state.f_external_full); result.state.converged = true; InMemoryResultsWriter writer; writer.writeLinearStatic(domain, dofs, result.state.u_full, result.state.reaction_full); result.result_file = writer.result(); } private: static const LinearSolver& defaultSolver() { static const GaussianEliminationSolver solver; return solver; } const LinearSolver* solver_ = nullptr; }; struct CsvDisplacementRow { GlobalId node_id = 0; std::array values{}; }; struct CsvDisplacementTable { std::map rows; std::vector diagnostics; }; inline std::vector displacementCsvRequiredColumns() { return {"Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}; } inline CsvDisplacementTable loadDisplacementCsvFromStream(std::istream& input, const std::string& source_name) { CsvDisplacementTable table; std::string line; if (!std::getline(input, line)) { table.diagnostics.push_back({Severity::Error, "FESA-CSV-EMPTY", "Displacement CSV is empty", {source_name, 1, ""}}); return table; } const std::vector required = displacementCsvRequiredColumns(); std::vector headers = splitCsv(line); std::map column; for (std::size_t i = 0; i < headers.size(); ++i) { column[trim(headers[i])] = i; } for (const std::string& name : required) { if (column.count(name) == 0) { table.diagnostics.push_back({Severity::Error, "FESA-CSV-MISSING-COLUMN", "Missing CSV column: " + name, {source_name, 1, ""}}); } } if (hasError(table.diagnostics)) { return table; } LocalIndex line_number = 1; while (std::getline(input, line)) { ++line_number; if (trim(line).empty()) { continue; } std::vector fields = splitCsv(line); auto get = [&](const std::string& name) -> std::string { const std::size_t index = column[name]; return index < fields.size() ? fields[index] : ""; }; auto node_id = parseInt64(get("Node Label")); if (!node_id) { table.diagnostics.push_back({Severity::Error, "FESA-CSV-NODE", "Invalid node label", {source_name, line_number, ""}}); continue; } if (table.rows.count(*node_id) != 0) { table.diagnostics.push_back({Severity::Error, "FESA-CSV-DUPLICATE-NODE", "Duplicate node label", {source_name, line_number, ""}}); continue; } CsvDisplacementRow row; row.node_id = *node_id; for (std::size_t i = 0; i < 6; ++i) { auto value = parseReal(get(required[i + 1])); if (!value) { table.diagnostics.push_back({Severity::Error, "FESA-CSV-NUMERIC", "Invalid displacement value", {source_name, line_number, ""}}); value = 0.0; } row.values[i] = *value; } table.rows[*node_id] = row; } return table; } inline CsvDisplacementTable loadDisplacementCsvFromString(const std::string& text, const std::string& source_name = "") { std::istringstream input(text); return loadDisplacementCsvFromStream(input, source_name); } inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) { std::ifstream input(path); if (!input.good()) { CsvDisplacementTable table; table.diagnostics.push_back({Severity::Error, "FESA-CSV-READ", "Could not read displacement CSV", {path, 0, ""}}); return table; } return loadDisplacementCsvFromStream(input, path); } struct ComparisonOptions { Real abs_tol = 1.0e-12; Real rel_tol = 1.0e-5; Real reference_scale = 1.0; }; struct ComparisonResult { bool pass = false; Real max_abs_error = 0.0; Real max_rel_error = 0.0; std::vector diagnostics; }; inline ComparisonResult compareDisplacements(const FieldOutput& actual, const CsvDisplacementTable& expected, ComparisonOptions options = {}) { ComparisonResult result; result.diagnostics = expected.diagnostics; if (hasError(result.diagnostics)) { return result; } if (actual.name != "U") { result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-FIELD-NAME", "Expected FESA displacement field named U", {}}); } if (actual.component_labels != displacementComponentLabels()) { result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-COMPONENT-LABELS", "FESA U field component labels must be UX,UY,UZ,RX,RY,RZ", {}}); } if (actual.position != "NODAL" || actual.entity_type != "node" || actual.basis != "GLOBAL") { result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-FIELD-METADATA", "FESA U field must be nodal values in the global basis", {}}); } if (actual.entity_ids.size() != actual.values.size()) { result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-FIELD-SIZE", "FESA U field entity/value counts differ", {}}); } std::map> actual_by_node; const std::size_t actual_count = std::min(actual.entity_ids.size(), actual.values.size()); for (std::size_t i = 0; i < actual_count; ++i) { if (actual_by_node.count(actual.entity_ids[i]) != 0) { result.diagnostics.push_back( {Severity::Error, "FESA-COMPARE-DUPLICATE-ACTUAL", "FESA U field contains duplicate node " + std::to_string(actual.entity_ids[i]), {}}); continue; } actual_by_node[actual.entity_ids[i]] = actual.values[i]; } for (const auto& [node_id, row] : expected.rows) { auto actual_it = actual_by_node.find(node_id); if (actual_it == actual_by_node.end()) { result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-MISSING-ACTUAL", "FESA U field is missing node " + std::to_string(node_id), {}}); continue; } for (std::size_t component = 0; component < 6; ++component) { const Real expected_value = row.values[component]; const Real actual_value = actual_it->second[component]; const Real abs_error = std::fabs(actual_value - expected_value); const Real scale = std::max(std::fabs(expected_value), std::fabs(options.reference_scale)); const Real rel_error = scale > 0.0 ? abs_error / scale : (abs_error == 0.0 ? 0.0 : std::numeric_limits::infinity()); result.max_abs_error = std::max(result.max_abs_error, abs_error); result.max_rel_error = std::max(result.max_rel_error, rel_error); if (!(abs_error <= options.abs_tol || rel_error <= options.rel_tol)) { const std::string component_label = displacementComponentLabels()[component]; result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-TOLERANCE", "Displacement comparison failed at node " + std::to_string(node_id) + " component " + component_label, {}}); } } } result.pass = !hasError(result.diagnostics); return result; } } // namespace fesa