feat: strengthen results comparator foundation

This commit is contained in:
NINI
2026-05-04 13:27:46 +09:00
parent b9b0963d50
commit 6de430f1ed
7 changed files with 229 additions and 32 deletions
+72 -20
View File
@@ -1463,6 +1463,10 @@ inline AssemblyResult assembleSystem(const Domain& domain, const DofManager& dof
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;
@@ -1470,8 +1474,12 @@ struct FieldOutput {
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;
};
@@ -1483,6 +1491,11 @@ struct ResultStep {
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;
@@ -1506,8 +1519,8 @@ class InMemoryResultsWriter {
step.name = domain.steps.empty() ? "Step-1" : domain.steps.front().name;
ResultFrame frame;
frame.frame_id = 0;
frame.field_outputs["U"] = buildNodalField("U", displacementComponentLabels(), domain, dofs, u_full);
frame.field_outputs["RF"] = buildNodalField("RF", reactionComponentLabels(), domain, dofs, rf_full);
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);
}
@@ -1517,10 +1530,14 @@ class InMemoryResultsWriter {
}
private:
static FieldOutput buildNodalField(const std::string& name, const std::vector<std::string>& labels, const Domain& domain,
const DofManager& dofs, const std::vector<Real>& full_values) {
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;
@@ -1625,19 +1642,18 @@ struct CsvDisplacementTable {
std::vector<Diagnostic> diagnostics;
};
inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
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::ifstream input(path);
if (!input.good()) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-READ", "Could not read displacement CSV", {path, 0, ""}});
return table;
}
std::string line;
if (!std::getline(input, line)) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-EMPTY", "Displacement CSV is empty", {path, 1, ""}});
table.diagnostics.push_back({Severity::Error, "FESA-CSV-EMPTY", "Displacement CSV is empty", {source_name, 1, ""}});
return table;
}
const std::vector<std::string> required = {"Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
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) {
@@ -1645,7 +1661,7 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
}
for (const std::string& name : required) {
if (column.count(name) == 0) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-MISSING-COLUMN", "Missing CSV column: " + name, {path, 1, ""}});
table.diagnostics.push_back({Severity::Error, "FESA-CSV-MISSING-COLUMN", "Missing CSV column: " + name, {source_name, 1, ""}});
}
}
if (hasError(table.diagnostics)) {
@@ -1664,11 +1680,11 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
};
auto node_id = parseInt64(get("Node Label"));
if (!node_id) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-NODE", "Invalid node label", {path, line_number, ""}});
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", {path, line_number, ""}});
table.diagnostics.push_back({Severity::Error, "FESA-CSV-DUPLICATE-NODE", "Duplicate node label", {source_name, line_number, ""}});
continue;
}
CsvDisplacementRow row;
@@ -1676,7 +1692,7 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
for (std::size_t i = 0; i < 6; ++i) {
auto value = parseReal(get(required[i + 1]));
if (!value) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-NUMERIC", "Invalid displacement value", {path, line_number, ""}});
table.diagnostics.push_back({Severity::Error, "FESA-CSV-NUMERIC", "Invalid displacement value", {source_name, line_number, ""}});
value = 0.0;
}
row.values[i] = *value;
@@ -1686,6 +1702,21 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
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;
@@ -1705,25 +1736,46 @@ inline ComparisonResult compareDisplacements(const FieldOutput& actual, const Cs
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;
for (std::size_t i = 0; i < actual.entity_ids.size(); ++i) {
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-NODE", "FESA output is missing node " + std::to_string(node_id), {}});
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 rel_error = abs_error / std::max(std::fabs(expected_value), options.reference_scale);
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)) {
result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-TOLERANCE", "Displacement comparison failed at node " + std::to_string(node_id), {}});
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, {}});
}
}
}