refactor: extract results reference comparison
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/Results/ResultModel.hpp"
|
||||
#include "fesa/Util/Util.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
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
|
||||
@@ -0,0 +1,123 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/Core/Core.hpp"
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fesa {
|
||||
|
||||
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_;
|
||||
};
|
||||
|
||||
} // namespace fesa
|
||||
@@ -1,6 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "fesa/ModuleInfo.hpp"
|
||||
#include "fesa/Results/ResultModel.hpp"
|
||||
#include "fesa/Results/ReferenceComparison.hpp"
|
||||
|
||||
namespace fesa::module {
|
||||
|
||||
|
||||
+1
-252
@@ -7,6 +7,7 @@
|
||||
#include "fesa/Math/Math.hpp"
|
||||
#include "fesa/ModuleInfo.hpp"
|
||||
#include "fesa/Property/Property.hpp"
|
||||
#include "fesa/Results/Results.hpp"
|
||||
#include "fesa/Util/Util.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -1069,107 +1070,6 @@ inline ReducedSystem projectToReducedSystem(const AssemblyResult& assembly, cons
|
||||
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;
|
||||
@@ -1273,155 +1173,4 @@ inline AnalysisResult runLinearStaticInputString(const std::string& text,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user