1735 lines
64 KiB
C++
1735 lines
64 KiB
C++
#pragma once
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <fstream>
|
|
#include <initializer_list>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
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<Diagnostic>& diagnostics) {
|
|
return std::any_of(diagnostics.begin(), diagnostics.end(), [](const Diagnostic& diagnostic) {
|
|
return diagnostic.severity == Severity::Error;
|
|
});
|
|
}
|
|
|
|
inline bool containsDiagnostic(const std::vector<Diagnostic>& 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 = "<domain>", 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<char>(std::tolower(c));
|
|
});
|
|
return text;
|
|
}
|
|
|
|
inline std::vector<std::string> splitCsv(const std::string& line) {
|
|
std::vector<std::string> 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<Real> 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<GlobalId> 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<GlobalId>(value);
|
|
} catch (...) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
enum class Dof : int { UX = 0, UY = 1, UZ = 2, RX = 3, RY = 4, RZ = 5 };
|
|
|
|
inline std::array<Dof, 6> allDofs() {
|
|
return {Dof::UX, Dof::UY, Dof::UZ, Dof::RX, Dof::RY, Dof::RZ};
|
|
}
|
|
|
|
inline int dofIndex(Dof dof) {
|
|
return static_cast<int>(dof);
|
|
}
|
|
|
|
inline int abaqusDofNumber(Dof dof) {
|
|
return dofIndex(dof) + 1;
|
|
}
|
|
|
|
inline std::optional<Dof> dofFromAbaqus(int dof) {
|
|
if (dof < 1 || dof > 6) {
|
|
return std::nullopt;
|
|
}
|
|
return static_cast<Dof>(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<std::string> displacementComponentLabels() {
|
|
return {"UX", "UY", "UZ", "RX", "RY", "RZ"};
|
|
}
|
|
|
|
inline std::vector<std::string> 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 Vec3 normalized(const Vec3& value) {
|
|
const Real length = norm(value);
|
|
if (length <= std::numeric_limits<Real>::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<GlobalId, 4> node_ids{};
|
|
std::string source_elset;
|
|
};
|
|
|
|
struct NodeSet {
|
|
std::string name;
|
|
std::vector<GlobalId> node_ids;
|
|
};
|
|
|
|
struct ElementSet {
|
|
std::string name;
|
|
std::vector<GlobalId> 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<GlobalId, Node> nodes;
|
|
std::map<GlobalId, Element> elements;
|
|
std::map<std::string, NodeSet> node_sets;
|
|
std::map<std::string, ElementSet> element_sets;
|
|
std::map<std::string, Material> materials;
|
|
std::vector<ShellSection> shell_sections;
|
|
std::vector<BoundaryCondition> boundary_conditions;
|
|
std::vector<NodalLoad> loads;
|
|
std::vector<StepDefinition> steps;
|
|
|
|
static std::string key(const std::string& label) {
|
|
return lower(trim(label));
|
|
}
|
|
};
|
|
|
|
inline void addUnique(std::vector<GlobalId>& values, GlobalId value) {
|
|
if (std::find(values.begin(), values.end(), value) == values.end()) {
|
|
values.push_back(value);
|
|
}
|
|
}
|
|
|
|
inline std::vector<GlobalId> generatedRange(GlobalId first, GlobalId last, GlobalId increment) {
|
|
std::vector<GlobalId> 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<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 std::optional<GlobalId> numericTarget(const std::string& target) {
|
|
return parseInt64(target);
|
|
}
|
|
|
|
inline std::vector<GlobalId> resolveNodeTarget(const Domain& domain, const std::string& target, std::vector<Diagnostic>* 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<Diagnostic> validateDomain(const Domain& domain) {
|
|
std::vector<Diagnostic> 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<std::pair<GlobalId, int>> 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<GlobalId> 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<LocalIndex>(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<EquationId>(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<LocalIndex>(all_dofs_.size());
|
|
}
|
|
|
|
LocalIndex freeDofCount() const {
|
|
return static_cast<LocalIndex>(free_full_indices_.size());
|
|
}
|
|
|
|
LocalIndex constrainedDofCount() const {
|
|
return static_cast<LocalIndex>(constrained_full_indices_.size());
|
|
}
|
|
|
|
const std::vector<GlobalId>& nodeIds() const {
|
|
return node_ids_;
|
|
}
|
|
|
|
const std::vector<LocalIndex>& freeFullIndices() const {
|
|
return free_full_indices_;
|
|
}
|
|
|
|
const std::vector<LocalIndex>& constrainedFullIndices() const {
|
|
return constrained_full_indices_;
|
|
}
|
|
|
|
DofAddress fullDof(LocalIndex full_index) const {
|
|
const auto& key = all_dofs_.at(static_cast<std::size_t>(full_index));
|
|
return {key.first, static_cast<Dof>(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<Real> reduceFullVector(const std::vector<Real>& full) const {
|
|
std::vector<Real> reduced;
|
|
reduced.reserve(free_full_indices_.size());
|
|
for (LocalIndex full_index : free_full_indices_) {
|
|
reduced.push_back(full.at(static_cast<std::size_t>(full_index)));
|
|
}
|
|
return reduced;
|
|
}
|
|
|
|
std::vector<Real> reconstructFullVector(const std::vector<Real>& reduced) const {
|
|
std::vector<Real> full(static_cast<std::size_t>(fullDofCount()), 0.0);
|
|
for (std::size_t i = 0; i < free_full_indices_.size(); ++i) {
|
|
full[static_cast<std::size_t>(free_full_indices_[i])] = reduced.at(i);
|
|
}
|
|
return full;
|
|
}
|
|
|
|
std::array<LocalIndex, 24> elementFullDofIndices(const Element& element) const {
|
|
std::array<LocalIndex, 24> indices{};
|
|
for (LocalIndex node = 0; node < 4; ++node) {
|
|
for (Dof dof : allDofs()) {
|
|
const LocalIndex local = 6 * node + dofIndex(dof);
|
|
indices[static_cast<std::size_t>(local)] = fullIndex(element.node_ids[static_cast<std::size_t>(node)], dof);
|
|
}
|
|
}
|
|
return indices;
|
|
}
|
|
|
|
std::array<EquationId, 24> elementEquationIds(const Element& element) const {
|
|
std::array<EquationId, 24> equations{};
|
|
for (LocalIndex node = 0; node < 4; ++node) {
|
|
for (Dof dof : allDofs()) {
|
|
const LocalIndex local = 6 * node + dofIndex(dof);
|
|
equations[static_cast<std::size_t>(local)] = equation(element.node_ids[static_cast<std::size_t>(node)], dof);
|
|
}
|
|
}
|
|
return equations;
|
|
}
|
|
|
|
private:
|
|
std::vector<GlobalId> node_ids_;
|
|
std::vector<std::pair<GlobalId, int>> all_dofs_;
|
|
std::set<std::pair<GlobalId, int>> constrained_;
|
|
std::map<std::pair<GlobalId, int>, LocalIndex> full_index_by_key_;
|
|
std::map<std::pair<GlobalId, int>, EquationId> equation_by_key_;
|
|
std::vector<LocalIndex> free_full_indices_;
|
|
std::vector<LocalIndex> constrained_full_indices_;
|
|
};
|
|
|
|
class DenseMatrix {
|
|
public:
|
|
DenseMatrix() = default;
|
|
DenseMatrix(LocalIndex rows, LocalIndex cols) : rows_(rows), cols_(cols), values_(static_cast<std::size_t>(rows * cols), 0.0) {}
|
|
|
|
LocalIndex rows() const {
|
|
return rows_;
|
|
}
|
|
|
|
LocalIndex cols() const {
|
|
return cols_;
|
|
}
|
|
|
|
Real& operator()(LocalIndex row, LocalIndex col) {
|
|
return values_[static_cast<std::size_t>(row * cols_ + col)];
|
|
}
|
|
|
|
Real operator()(LocalIndex row, LocalIndex col) const {
|
|
return values_[static_cast<std::size_t>(row * cols_ + col)];
|
|
}
|
|
|
|
void add(LocalIndex row, LocalIndex col, Real value) {
|
|
(*this)(row, col) += value;
|
|
}
|
|
|
|
std::vector<Real> multiply(const std::vector<Real>& x) const {
|
|
std::vector<Real> y(static_cast<std::size_t>(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<std::size_t>(j)];
|
|
}
|
|
y[static_cast<std::size_t>(i)] = sum;
|
|
}
|
|
return y;
|
|
}
|
|
|
|
private:
|
|
LocalIndex rows_ = 0;
|
|
LocalIndex cols_ = 0;
|
|
std::vector<Real> values_;
|
|
};
|
|
|
|
inline std::vector<Real> recoverFullReaction(const DenseMatrix& k_full, const std::vector<Real>& u_full, const std::vector<Real>& f_full) {
|
|
if (k_full.rows() != k_full.cols() || static_cast<LocalIndex>(u_full.size()) != k_full.cols() ||
|
|
static_cast<LocalIndex>(f_full.size()) != k_full.rows()) {
|
|
throw std::runtime_error("full reaction size mismatch");
|
|
}
|
|
std::vector<Real> 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<Real> x;
|
|
std::vector<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
class LinearSolver {
|
|
public:
|
|
virtual ~LinearSolver() = default;
|
|
virtual SolveResult solve(DenseMatrix a, std::vector<Real> b) const = 0;
|
|
};
|
|
|
|
class GaussianEliminationSolver final : public LinearSolver {
|
|
public:
|
|
SolveResult solve(DenseMatrix a, std::vector<Real> b) const override {
|
|
const LocalIndex n = a.rows();
|
|
SolveResult result;
|
|
if (a.rows() != a.cols() || static_cast<LocalIndex>(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<std::size_t>(col)], b[static_cast<std::size_t>(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<std::size_t>(row)] -= factor * b[static_cast<std::size_t>(col)];
|
|
}
|
|
}
|
|
result.x.assign(static_cast<std::size_t>(n), 0.0);
|
|
for (LocalIndex i = n; i-- > 0;) {
|
|
Real sum = b[static_cast<std::size_t>(i)];
|
|
for (LocalIndex j = i + 1; j < n; ++j) {
|
|
sum -= a(i, j) * result.x[static_cast<std::size_t>(j)];
|
|
}
|
|
result.x[static_cast<std::size_t>(i)] = sum / a(i, i);
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
struct ShapeData {
|
|
std::array<Real, 4> n{};
|
|
std::array<Real, 4> dr{};
|
|
std::array<Real, 4> 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;
|
|
};
|
|
|
|
inline LocalBasis computeLocalBasis(const std::array<Vec3, 4>& coordinates) {
|
|
Vec3 v1 = 0.5 * ((coordinates[1] - coordinates[0]) + (coordinates[2] - coordinates[3]));
|
|
Vec3 v2 = 0.5 * ((coordinates[3] - coordinates[0]) + (coordinates[2] - coordinates[1]));
|
|
Vec3 e1 = normalized(v1);
|
|
v2 = v2 - dot(v2, e1) * e1;
|
|
Vec3 e2 = normalized(v2);
|
|
Vec3 e3 = normalized(cross(e1, e2));
|
|
e2 = normalized(cross(e3, e1));
|
|
return {e1, e2, e3};
|
|
}
|
|
|
|
struct NaturalDerivatives {
|
|
ShapeData shape;
|
|
std::array<Real, 4> dx{};
|
|
std::array<Real, 4> dy{};
|
|
Real det_j = 0.0;
|
|
};
|
|
|
|
inline NaturalDerivatives naturalDerivatives(const std::array<std::array<Real, 2>, 4>& xy, Real r, Real s) {
|
|
NaturalDerivatives data;
|
|
data.shape = shapeFunctions(r, s);
|
|
Real j00 = 0.0;
|
|
Real j01 = 0.0;
|
|
Real j10 = 0.0;
|
|
Real j11 = 0.0;
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
j00 += data.shape.dr[i] * xy[i][0];
|
|
j01 += data.shape.ds[i] * xy[i][0];
|
|
j10 += data.shape.dr[i] * xy[i][1];
|
|
j11 += data.shape.ds[i] * xy[i][1];
|
|
}
|
|
data.det_j = j00 * j11 - j01 * j10;
|
|
if (std::fabs(data.det_j) < 1.0e-14) {
|
|
throw std::runtime_error("invalid shell element jacobian");
|
|
}
|
|
const Real inv00 = j11 / data.det_j;
|
|
const Real inv01 = -j01 / data.det_j;
|
|
const Real inv10 = -j10 / data.det_j;
|
|
const Real inv11 = j00 / data.det_j;
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
data.dx[i] = inv00 * data.shape.dr[i] + inv01 * data.shape.ds[i];
|
|
data.dy[i] = inv10 * data.shape.dr[i] + inv11 * data.shape.ds[i];
|
|
}
|
|
return data;
|
|
}
|
|
|
|
struct ElementStiffnessOptions {
|
|
Real drilling_stiffness_scale = 1.0e-6;
|
|
};
|
|
|
|
class MITC4ElementKernel {
|
|
public:
|
|
DenseMatrix stiffness(const std::array<Vec3, 4>& coordinates, Real elastic_modulus, Real poisson_ratio, Real thickness,
|
|
ElementStiffnessOptions options = {}) const {
|
|
const LocalBasis basis = computeLocalBasis(coordinates);
|
|
std::array<std::array<Real, 2>, 4> xy{};
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
const Vec3 relative = coordinates[i] - coordinates[0];
|
|
xy[i] = {dot(relative, basis.e1), dot(relative, basis.e2)};
|
|
}
|
|
|
|
DenseMatrix local(24, 24);
|
|
const Real membrane_factor = elastic_modulus * thickness / (1.0 - poisson_ratio * poisson_ratio);
|
|
const Real bending_factor = elastic_modulus * thickness * thickness * thickness / (12.0 * (1.0 - poisson_ratio * poisson_ratio));
|
|
const Real shear_factor = (5.0 / 6.0) * elastic_modulus * thickness / (2.0 * (1.0 + poisson_ratio));
|
|
const Real gauss = 1.0 / std::sqrt(3.0);
|
|
const std::array<Real, 2> points = {-gauss, gauss};
|
|
|
|
for (Real r : points) {
|
|
for (Real s : points) {
|
|
const NaturalDerivatives d = naturalDerivatives(xy, r, s);
|
|
DenseMatrix b(8, 24);
|
|
addMembraneB(b, d);
|
|
addBendingB(b, d);
|
|
addMitcShearB(b, xy, r, s);
|
|
accumulateBtDB(local, b, membrane_factor, bending_factor, shear_factor, poisson_ratio, std::fabs(d.det_j));
|
|
addDrilling(local, d, elastic_modulus * thickness * options.drilling_stiffness_scale, std::fabs(d.det_j));
|
|
}
|
|
}
|
|
|
|
return transformToGlobal(local, basis);
|
|
}
|
|
|
|
private:
|
|
static void addMembraneB(DenseMatrix& b, const NaturalDerivatives& d) {
|
|
for (LocalIndex i = 0; i < 4; ++i) {
|
|
const LocalIndex c = 6 * i;
|
|
b(0, c + 0) = d.dx[static_cast<std::size_t>(i)];
|
|
b(1, c + 1) = d.dy[static_cast<std::size_t>(i)];
|
|
b(2, c + 0) = d.dy[static_cast<std::size_t>(i)];
|
|
b(2, c + 1) = d.dx[static_cast<std::size_t>(i)];
|
|
}
|
|
}
|
|
|
|
static void addBendingB(DenseMatrix& b, const NaturalDerivatives& d) {
|
|
for (LocalIndex i = 0; i < 4; ++i) {
|
|
const LocalIndex c = 6 * i;
|
|
b(3, c + 4) = -d.dx[static_cast<std::size_t>(i)];
|
|
b(4, c + 3) = d.dy[static_cast<std::size_t>(i)];
|
|
b(5, c + 3) = d.dx[static_cast<std::size_t>(i)];
|
|
b(5, c + 4) = -d.dy[static_cast<std::size_t>(i)];
|
|
}
|
|
}
|
|
|
|
static void addStandardShearRow(DenseMatrix& b, LocalIndex row, const NaturalDerivatives& d, bool gamma_xz, Real scale) {
|
|
for (LocalIndex i = 0; i < 4; ++i) {
|
|
const LocalIndex c = 6 * i;
|
|
if (gamma_xz) {
|
|
b(row, c + 2) += scale * d.dx[static_cast<std::size_t>(i)];
|
|
b(row, c + 4) += scale * d.shape.n[static_cast<std::size_t>(i)];
|
|
} else {
|
|
b(row, c + 2) += scale * d.dy[static_cast<std::size_t>(i)];
|
|
b(row, c + 3) -= scale * d.shape.n[static_cast<std::size_t>(i)];
|
|
}
|
|
}
|
|
}
|
|
|
|
static void addMitcShearB(DenseMatrix& b, const std::array<std::array<Real, 2>, 4>& xy, Real r, Real s) {
|
|
addStandardShearRow(b, 6, naturalDerivatives(xy, 0.0, -1.0), true, 0.5 * (1.0 - s));
|
|
addStandardShearRow(b, 6, naturalDerivatives(xy, 0.0, 1.0), true, 0.5 * (1.0 + s));
|
|
addStandardShearRow(b, 7, naturalDerivatives(xy, -1.0, 0.0), false, 0.5 * (1.0 - r));
|
|
addStandardShearRow(b, 7, naturalDerivatives(xy, 1.0, 0.0), false, 0.5 * (1.0 + r));
|
|
}
|
|
|
|
static void accumulateBtDB(DenseMatrix& k, const DenseMatrix& b, Real membrane_factor, Real bending_factor, Real shear_factor, Real poisson_ratio, Real det_j) {
|
|
std::array<std::array<Real, 8>, 8> d{};
|
|
d[0][0] = membrane_factor;
|
|
d[0][1] = poisson_ratio * membrane_factor;
|
|
d[1][0] = poisson_ratio * membrane_factor;
|
|
d[1][1] = membrane_factor;
|
|
d[2][2] = membrane_factor * (1.0 - poisson_ratio) / 2.0;
|
|
d[3][3] = bending_factor;
|
|
d[3][4] = poisson_ratio * bending_factor;
|
|
d[4][3] = poisson_ratio * bending_factor;
|
|
d[4][4] = bending_factor;
|
|
d[5][5] = bending_factor * (1.0 - poisson_ratio) / 2.0;
|
|
d[6][6] = shear_factor;
|
|
d[7][7] = shear_factor;
|
|
for (LocalIndex i = 0; i < 24; ++i) {
|
|
for (LocalIndex j = 0; j < 24; ++j) {
|
|
Real value = 0.0;
|
|
for (LocalIndex row = 0; row < 8; ++row) {
|
|
for (LocalIndex col = 0; col < 8; ++col) {
|
|
value += b(row, i) * d[static_cast<std::size_t>(row)][static_cast<std::size_t>(col)] * b(col, j);
|
|
}
|
|
}
|
|
k.add(i, j, value * det_j);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void addDrilling(DenseMatrix& k, const NaturalDerivatives& d, Real drill_factor, Real det_j) {
|
|
std::array<Real, 24> b{};
|
|
for (LocalIndex i = 0; i < 4; ++i) {
|
|
const LocalIndex c = 6 * i;
|
|
b[static_cast<std::size_t>(c + 0)] = -0.5 * d.dy[static_cast<std::size_t>(i)];
|
|
b[static_cast<std::size_t>(c + 1)] = 0.5 * d.dx[static_cast<std::size_t>(i)];
|
|
b[static_cast<std::size_t>(c + 5)] = -d.shape.n[static_cast<std::size_t>(i)];
|
|
}
|
|
for (LocalIndex i = 0; i < 24; ++i) {
|
|
for (LocalIndex j = 0; j < 24; ++j) {
|
|
k.add(i, j, b[static_cast<std::size_t>(i)] * drill_factor * b[static_cast<std::size_t>(j)] * det_j);
|
|
}
|
|
}
|
|
}
|
|
|
|
static DenseMatrix transformToGlobal(const DenseMatrix& local, const LocalBasis& basis) {
|
|
DenseMatrix transform(24, 24);
|
|
const std::array<Vec3, 3> axes = {basis.e1, basis.e2, basis.e3};
|
|
for (LocalIndex node = 0; node < 4; ++node) {
|
|
for (LocalIndex local_axis = 0; local_axis < 3; ++local_axis) {
|
|
const Vec3 axis = axes[static_cast<std::size_t>(local_axis)];
|
|
const LocalIndex base = 6 * node;
|
|
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;
|
|
}
|
|
}
|
|
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 += transform(a, i) * local(a, b) * transform(b, j);
|
|
}
|
|
}
|
|
global(i, j) = value;
|
|
}
|
|
}
|
|
return global;
|
|
}
|
|
};
|
|
|
|
struct AssemblyResult {
|
|
DenseMatrix k_full;
|
|
std::vector<Real> f_full;
|
|
std::vector<Diagnostic> diagnostics;
|
|
};
|
|
|
|
inline AssemblyResult assembleSystem(const Domain& domain, const DofManager& dofs, ElementStiffnessOptions options = {}) {
|
|
AssemblyResult result{DenseMatrix(dofs.fullDofCount(), dofs.fullDofCount()), std::vector<Real>(static_cast<std::size_t>(dofs.fullDofCount()), 0.0), {}};
|
|
MITC4ElementKernel kernel;
|
|
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<Vec3, 4> coordinates{};
|
|
for (std::size_t i = 0; i < 4; ++i) {
|
|
coordinates[i] = domain.nodes.at(element.node_ids[i]).coordinates;
|
|
}
|
|
DenseMatrix ke = kernel.stiffness(coordinates, material_it->second.elastic_modulus, material_it->second.poisson_ratio, section->thickness, options);
|
|
const auto element_full_indices = dofs.elementFullDofIndices(element);
|
|
for (LocalIndex a = 0; a < 24; ++a) {
|
|
const LocalIndex ia = element_full_indices[static_cast<std::size_t>(a)];
|
|
for (LocalIndex b = 0; b < 24; ++b) {
|
|
const LocalIndex ib = element_full_indices[static_cast<std::size_t>(b)];
|
|
result.k_full.add(ia, ib, ke(a, b));
|
|
}
|
|
}
|
|
}
|
|
for (const NodalLoad& load : domain.loads) {
|
|
for (GlobalId node_id : resolveNodeTarget(domain, load.target, &result.diagnostics)) {
|
|
result.f_full[static_cast<std::size_t>(dofs.fullIndex(node_id, *dofFromAbaqus(load.dof)))] += load.magnitude;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
struct FieldOutput {
|
|
std::string name;
|
|
std::vector<GlobalId> entity_ids;
|
|
std::vector<std::string> component_labels;
|
|
std::vector<std::array<Real, 6>> values;
|
|
};
|
|
|
|
struct ResultFrame {
|
|
LocalIndex frame_id = 0;
|
|
Real step_time = 1.0;
|
|
Real total_time = 1.0;
|
|
std::map<std::string, FieldOutput> field_outputs;
|
|
};
|
|
|
|
struct ResultStep {
|
|
std::string name;
|
|
std::vector<ResultFrame> frames;
|
|
};
|
|
|
|
struct ResultFile {
|
|
std::string schema_name = "FESA_RESULTS";
|
|
LocalIndex schema_version = 1;
|
|
std::vector<GlobalId> node_ids;
|
|
std::vector<Vec3> coordinates;
|
|
std::vector<GlobalId> element_ids;
|
|
std::vector<std::array<GlobalId, 4>> connectivity;
|
|
std::vector<ResultStep> steps;
|
|
};
|
|
|
|
class InMemoryResultsWriter {
|
|
public:
|
|
void writeLinearStatic(const Domain& domain, const DofManager& dofs, const std::vector<Real>& u_full, const std::vector<Real>& 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(), domain, dofs, u_full);
|
|
frame.field_outputs["RF"] = buildNodalField("RF", reactionComponentLabels(), 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<std::string>& labels, const Domain& domain,
|
|
const DofManager& dofs, const std::vector<Real>& full_values) {
|
|
FieldOutput field;
|
|
field.name = name;
|
|
field.component_labels = labels;
|
|
for (const auto& [node_id, node] : domain.nodes) {
|
|
(void)node;
|
|
field.entity_ids.push_back(node_id);
|
|
std::array<Real, 6> values{};
|
|
for (Dof dof : allDofs()) {
|
|
values[static_cast<std::size_t>(dofIndex(dof))] = full_values[static_cast<std::size_t>(dofs.fullIndex(node_id, dof))];
|
|
}
|
|
field.values.push_back(values);
|
|
}
|
|
return field;
|
|
}
|
|
|
|
ResultFile result_;
|
|
};
|
|
|
|
struct AnalysisState {
|
|
std::vector<Real> u_full;
|
|
std::vector<Real> f_external_full;
|
|
std::vector<Real> reaction_full;
|
|
bool converged = false;
|
|
};
|
|
|
|
struct AnalysisResult {
|
|
AnalysisState state;
|
|
ResultFile result_file;
|
|
std::vector<Diagnostic> 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 {
|
|
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;
|
|
}
|
|
DenseMatrix k_reduced(dofs.freeDofCount(), dofs.freeDofCount());
|
|
std::vector<Real> f_reduced(static_cast<std::size_t>(dofs.freeDofCount()), 0.0);
|
|
for (LocalIndex i = 0; i < dofs.freeDofCount(); ++i) {
|
|
const LocalIndex full_i = dofs.freeFullIndices()[static_cast<std::size_t>(i)];
|
|
f_reduced[static_cast<std::size_t>(i)] = assembly.f_full[static_cast<std::size_t>(full_i)];
|
|
for (LocalIndex j = 0; j < dofs.freeDofCount(); ++j) {
|
|
const LocalIndex full_j = dofs.freeFullIndices()[static_cast<std::size_t>(j)];
|
|
k_reduced(i, j) = assembly.k_full(full_i, full_j);
|
|
}
|
|
}
|
|
GaussianEliminationSolver solver;
|
|
SolveResult solved = solver.solve(k_reduced, f_reduced);
|
|
result.diagnostics.insert(result.diagnostics.end(), solved.diagnostics.begin(), solved.diagnostics.end());
|
|
if (!solved.ok()) {
|
|
return;
|
|
}
|
|
result.state.u_full = dofs.reconstructFullVector(solved.x);
|
|
result.state.f_external_full = assembly.f_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();
|
|
}
|
|
};
|
|
|
|
struct CsvDisplacementRow {
|
|
GlobalId node_id = 0;
|
|
std::array<Real, 6> values{};
|
|
};
|
|
|
|
struct CsvDisplacementTable {
|
|
std::map<GlobalId, CsvDisplacementRow> rows;
|
|
std::vector<Diagnostic> diagnostics;
|
|
};
|
|
|
|
inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
|
|
CsvDisplacementTable table;
|
|
std::ifstream input(path);
|
|
if (!input.good()) {
|
|
table.diagnostics.push_back({Severity::Error, "FESA-CSV-READ", "Could not read displacement CSV", {path, 0, ""}});
|
|
return table;
|
|
}
|
|
std::string line;
|
|
if (!std::getline(input, line)) {
|
|
table.diagnostics.push_back({Severity::Error, "FESA-CSV-EMPTY", "Displacement CSV is empty", {path, 1, ""}});
|
|
return table;
|
|
}
|
|
const std::vector<std::string> required = {"Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
|
|
std::vector<std::string> headers = splitCsv(line);
|
|
std::map<std::string, std::size_t> 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, {path, 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<std::string> 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", {path, line_number, ""}});
|
|
continue;
|
|
}
|
|
if (table.rows.count(*node_id) != 0) {
|
|
table.diagnostics.push_back({Severity::Error, "FESA-CSV-DUPLICATE-NODE", "Duplicate node label", {path, 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", {path, line_number, ""}});
|
|
value = 0.0;
|
|
}
|
|
row.values[i] = *value;
|
|
}
|
|
table.rows[*node_id] = row;
|
|
}
|
|
return table;
|
|
}
|
|
|
|
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<Diagnostic> diagnostics;
|
|
};
|
|
|
|
inline ComparisonResult compareDisplacements(const FieldOutput& actual, const CsvDisplacementTable& expected, ComparisonOptions options = {}) {
|
|
ComparisonResult result;
|
|
result.diagnostics = expected.diagnostics;
|
|
if (hasError(result.diagnostics)) {
|
|
return result;
|
|
}
|
|
std::map<GlobalId, std::array<Real, 6>> actual_by_node;
|
|
for (std::size_t i = 0; i < actual.entity_ids.size(); ++i) {
|
|
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-NODE", "FESA output 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 rel_error = abs_error / std::max(std::fabs(expected_value), options.reference_scale);
|
|
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)) {
|
|
result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-TOLERANCE", "Displacement comparison failed at node " + std::to_string(node_id), {}});
|
|
}
|
|
}
|
|
}
|
|
result.pass = !hasError(result.diagnostics);
|
|
return result;
|
|
}
|
|
|
|
} // namespace fesa
|