2047 lines
78 KiB
C++
2047 lines
78 KiB
C++
#pragma once
|
|
|
|
#include "fesa/Boundary/Boundary.hpp"
|
|
#include "fesa/Core/Core.hpp"
|
|
#include "fesa/Load/Load.hpp"
|
|
#include "fesa/ModuleInfo.hpp"
|
|
#include "fesa/Property/Property.hpp"
|
|
#include "fesa/Util/Util.hpp"
|
|
|
|
#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 {
|
|
|
|
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<Vec3> 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<Real>::epsilon()) {
|
|
throw std::runtime_error("zero-length vector");
|
|
}
|
|
return (1.0 / length) * value;
|
|
}
|
|
|
|
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});
|
|
}
|
|
};
|
|
|
|
struct SparsePatternEntry {
|
|
EquationId row = 0;
|
|
EquationId col = 0;
|
|
};
|
|
|
|
struct SparsePattern {
|
|
EquationId equation_count = 0;
|
|
std::vector<SparsePatternEntry> entries;
|
|
|
|
SparseIndex nonzeroCount() const {
|
|
return static_cast<SparseIndex>(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<std::pair<EquationId, EquationId>> 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<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;
|
|
};
|
|
|
|
struct MITC4NaturalPoint {
|
|
Real xi = 0.0;
|
|
Real eta = 0.0;
|
|
};
|
|
|
|
struct MITC4TyingPoint {
|
|
std::string label;
|
|
MITC4NaturalPoint natural;
|
|
std::array<LocalIndex, 2> edge_node_indices{};
|
|
};
|
|
|
|
inline std::array<MITC4NaturalPoint, 4> mitc4NodeNaturalCoordinates() {
|
|
return {{{-1.0, -1.0}, {1.0, -1.0}, {1.0, 1.0}, {-1.0, 1.0}}};
|
|
}
|
|
|
|
inline std::array<MITC4TyingPoint, 4> 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<Vec3, 4> coordinates{};
|
|
Real thickness = 0.0;
|
|
ShapeData center_shape;
|
|
Vec3 g1_center;
|
|
Vec3 g2_center;
|
|
Vec3 center_normal;
|
|
std::array<MITC4DirectorFrame, 4> nodal_frames{};
|
|
std::vector<Diagnostic> 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<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
using MITC4ElementDofVector = std::array<Real, 24>;
|
|
using MITC4StrainVector = std::array<Real, 6>;
|
|
using MITC4StrainRow = std::array<Real, 24>;
|
|
using MITC4MaterialMatrix = std::array<std::array<Real, 6>, 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<std::size_t>(component);
|
|
}
|
|
|
|
inline std::array<std::string, 6> 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<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct MITC4StrainEvaluation {
|
|
MITC4StrainVector values{};
|
|
std::vector<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct MITC4StrainRows {
|
|
std::array<MITC4StrainRow, 6> rows{};
|
|
std::vector<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct MITC4MaterialMatrixEvaluation {
|
|
MITC4MaterialMatrix matrix{};
|
|
std::vector<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct MITC4StrainTransform {
|
|
MITC4MaterialMatrix matrix{};
|
|
std::vector<Diagnostic> 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<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct MITC4MaterialIntegrationData {
|
|
std::vector<MITC4MaterialIntegrationSample> samples;
|
|
std::vector<Diagnostic> 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", "<element>", 0);
|
|
}
|
|
|
|
inline void appendDiagnostics(std::vector<Diagnostic>& target, const std::vector<Diagnostic>& source) {
|
|
target.insert(target.end(), source.begin(), source.end());
|
|
}
|
|
|
|
inline MITC4MidsurfaceDerivatives mitc4MidsurfaceDerivatives(const std::array<Vec3, 4>& 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<Vec3> firstNormalizedCross(const std::array<Vec3, 3>& 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<MITC4DirectorFrame> 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<Vec3, 4>& 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<MITC4IntegrationPoint, 8> mitc4GaussQuadrature2x2x2() {
|
|
const Real gauss = 1.0 / std::sqrt(3.0);
|
|
const std::array<Real, 2> points = {-gauss, gauss};
|
|
std::array<MITC4IntegrationPoint, 8> 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<std::array<Real, 3>, 3> mitc4TensorFromEngineeringComponent(std::size_t component) {
|
|
std::array<std::array<Real, 3>, 3> tensor{};
|
|
switch (static_cast<MITC4StrainComponent>(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<std::array<Real, 3>, 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<Vec3, 3> 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<Vec3, 3> local = {basis.local.e1, basis.local.e2, basis.local.e3};
|
|
std::array<std::array<Real, 3>, 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<std::array<Real, 3>, 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<Vec3, 4>& 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<Diagnostic> 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<Diagnostic> 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<std::size_t>(node)];
|
|
const std::array<Vec3, 3> axes = {frame.v1, frame.v2, frame.vn};
|
|
for (LocalIndex local_axis = 0; local_axis < 3; ++local_axis) {
|
|
const Vec3 axis = axes[static_cast<std::size_t>(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<std::size_t>(global_dof)] *
|
|
local_dof_transform(local_dof, global_dof);
|
|
}
|
|
local_rows.rows[component][static_cast<std::size_t>(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<std::size_t>(i)] * material[a][b] *
|
|
rows.rows[b][static_cast<std::size_t>(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<Real>::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<Vec3, 4>& 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<Real> mitc4ElementInternalForce(const MITC4ElementStiffnessResult& stiffness,
|
|
const std::vector<Real>& element_displacement) {
|
|
if (stiffness.global.rows() != static_cast<LocalIndex>(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<Vec3, 4>& 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<Real> f_full;
|
|
SparsePattern reduced_pattern;
|
|
std::vector<Diagnostic> diagnostics;
|
|
|
|
bool ok() const {
|
|
return !hasError(diagnostics);
|
|
}
|
|
};
|
|
|
|
struct ReducedSystem {
|
|
DenseMatrix k;
|
|
std::vector<Real> f;
|
|
std::vector<LocalIndex> free_full_indices;
|
|
std::vector<Diagnostic> 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<Real>(static_cast<std::size_t>(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<Vec3, 4> 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<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, 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<std::size_t>(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<Real>(static_cast<std::size_t>(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<LocalIndex>(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<std::size_t>(i)];
|
|
result.f[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)];
|
|
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<GlobalId> entity_ids;
|
|
std::vector<std::string> component_labels;
|
|
std::vector<std::array<Real, 6>> 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<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::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<GlobalId> node_ids;
|
|
std::vector<Vec3> coordinates;
|
|
std::vector<GlobalId> element_ids;
|
|
std::vector<std::string> element_types;
|
|
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) {
|
|
const auto model = buildLinearStaticAnalysisModel(domain);
|
|
writeLinearStatic(domain, model, dofs, u_full, rf_full);
|
|
}
|
|
|
|
void writeLinearStatic(const Domain& domain, const AnalysisModel& model, 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_.element_types.push_back(elementTypeLabel(element.type));
|
|
result_.connectivity.push_back(element.node_ids);
|
|
}
|
|
ResultStep step;
|
|
step.name = model.step.name.empty() ? "Step-1" : model.step.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<std::string>& labels, const std::string& description,
|
|
const Domain& domain, const DofManager& dofs, const std::vector<Real>& 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<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 AnalysisResult {
|
|
AnalysisModel model;
|
|
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 {
|
|
public:
|
|
explicit LinearStaticAnalysis(const LinearSolver* solver = nullptr) : solver_(solver) {}
|
|
|
|
protected:
|
|
void solve(const Domain& domain, AnalysisResult& result) const override {
|
|
result.model = buildLinearStaticAnalysisModel(domain);
|
|
result.diagnostics.insert(result.diagnostics.end(), result.model.diagnostics.begin(), result.model.diagnostics.end());
|
|
if (hasError(result.diagnostics)) {
|
|
return;
|
|
}
|
|
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<LocalIndex>(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, result.model, 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;
|
|
};
|
|
|
|
inline AnalysisResult runLinearStaticInputString(const std::string& text,
|
|
const std::string& source_name = "<memory>",
|
|
const LinearSolver* solver = nullptr) {
|
|
AbaqusInputParser parser;
|
|
const auto parsed = parser.parseString(text, source_name);
|
|
if (!parsed.ok()) {
|
|
AnalysisResult result;
|
|
result.diagnostics = parsed.diagnostics;
|
|
return result;
|
|
}
|
|
LinearStaticAnalysis analysis(solver);
|
|
return analysis.run(parsed.domain);
|
|
}
|
|
|
|
struct CsvDisplacementRow {
|
|
GlobalId node_id = 0;
|
|
std::array<Real, 6> values{};
|
|
};
|
|
|
|
struct CsvDisplacementTable {
|
|
std::map<GlobalId, CsvDisplacementRow> rows;
|
|
std::vector<Diagnostic> diagnostics;
|
|
};
|
|
|
|
inline std::vector<std::string> 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<std::string> required = displacementCsvRequiredColumns();
|
|
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, {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<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", {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 = "<memory>") {
|
|
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<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;
|
|
}
|
|
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<GlobalId, std::array<Real, 6>> 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<Real>::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
|