refactor: extract results reference comparison
This commit is contained in:
@@ -28,21 +28,28 @@ add_executable(fesa_io_module_tests tests/test_io_module_includes.cpp)
|
||||
target_link_libraries(fesa_io_module_tests PRIVATE fesa_core)
|
||||
target_compile_definitions(fesa_io_module_tests PRIVATE FESA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
add_executable(fesa_results_module_tests tests/test_results_module_includes.cpp)
|
||||
target_link_libraries(fesa_results_module_tests PRIVATE fesa_core)
|
||||
target_compile_definitions(fesa_results_module_tests PRIVATE FESA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
||||
|
||||
if(MSVC)
|
||||
target_compile_options(fesa_core PRIVATE /W4 /permissive-)
|
||||
target_compile_options(fesa_tests PRIVATE /W4 /permissive-)
|
||||
target_compile_options(fesa_core_module_tests PRIVATE /W4 /permissive-)
|
||||
target_compile_options(fesa_math_module_tests PRIVATE /W4 /permissive-)
|
||||
target_compile_options(fesa_io_module_tests PRIVATE /W4 /permissive-)
|
||||
target_compile_options(fesa_results_module_tests PRIVATE /W4 /permissive-)
|
||||
else()
|
||||
target_compile_options(fesa_core PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(fesa_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(fesa_core_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(fesa_math_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(fesa_io_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
target_compile_options(fesa_results_module_tests PRIVATE -Wall -Wextra -Wpedantic)
|
||||
endif()
|
||||
|
||||
add_test(NAME fesa_tests COMMAND fesa_tests)
|
||||
add_test(NAME fesa_core_module_tests COMMAND fesa_core_module_tests)
|
||||
add_test(NAME fesa_math_module_tests COMMAND fesa_math_module_tests)
|
||||
add_test(NAME fesa_io_module_tests COMMAND fesa_io_module_tests)
|
||||
add_test(NAME fesa_results_module_tests COMMAND fesa_results_module_tests)
|
||||
|
||||
@@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p
|
||||
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
||||
|
||||
## Current Objective
|
||||
Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor`, continuing with P1A-05 Results/reference extraction. P1A-00 completed the architecture drift audit, P1A-01 created the module scaffold, P1A-02 extracted Core/Util plus Phase 1 Boundary/Load/Property model ownership, P1A-03 extracted Math primitives plus the solver adapter boundary, and P1A-04 extracted the Abaqus Phase 1 parser into IO without changing solver behavior. This phase must align the current monolithic `include/fesa/fesa.hpp` implementation with the module ownership model in `docs/ARCHITECTURE.md` without changing solver behavior. Product-level Phase 1 reference gaps R-010 and R-013 remain open and must not be hidden by the refactor.
|
||||
Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor`, continuing with P1A-06 MITC4 geometry/strain extraction. P1A-00 completed the architecture drift audit, P1A-01 created the module scaffold, P1A-02 extracted Core/Util plus Phase 1 Boundary/Load/Property model ownership, P1A-03 extracted Math primitives plus the solver adapter boundary, P1A-04 extracted the Abaqus Phase 1 parser into IO, and P1A-05 extracted Results/reference comparison without changing solver behavior. This phase must align the current monolithic `include/fesa/fesa.hpp` implementation with the module ownership model in `docs/ARCHITECTURE.md` without changing solver behavior. Product-level Phase 1 reference gaps R-010 and R-013 remain open and must not be hidden by the refactor.
|
||||
|
||||
## Required Reading For New Agents
|
||||
1. `AGENTS.md`
|
||||
@@ -37,7 +37,7 @@ Execute the Phase 1 structure-alignment refactor in `phases/1-structure-alignmen
|
||||
## Phase Files
|
||||
- Active phase directory: `phases/1-structure-alignment-refactor`
|
||||
- Execute with: `python scripts/execute.py 1-structure-alignment-refactor`
|
||||
- Step numbering is zero-based. `step0.md` is complete and wrote `phases/1-structure-alignment-refactor/step0-architecture-map.md`; `step1.md` is complete and created module scaffold headers, source directories, CMake source discovery, and umbrella compatibility smoke coverage; `step2.md` is complete and extracted Core/Util domain, diagnostics, DofManager ownership, AnalysisModel/AnalysisState, and Phase 1 Boundary/Load/Property model ownership; `step3.md` is complete and extracted Math primitives, sparse pattern data, dense matrix support, and solver adapter boundary; `step4.md` is complete and extracted the Abaqus parser into IO; `step5.md` extracts Results and reference comparison code; `step6.md` extracts MITC4 geometry/strain helpers; `step7.md` extracts MITC4 material/stiffness helpers; `step8.md` extracts Assembly and Analysis workflow; `step9.md` is the independent architecture evaluator closeout.
|
||||
- Step numbering is zero-based. `step0.md` is complete and wrote `phases/1-structure-alignment-refactor/step0-architecture-map.md`; `step1.md` is complete and created module scaffold headers, source directories, CMake source discovery, and umbrella compatibility smoke coverage; `step2.md` is complete and extracted Core/Util domain, diagnostics, DofManager ownership, AnalysisModel/AnalysisState, and Phase 1 Boundary/Load/Property model ownership; `step3.md` is complete and extracted Math primitives, sparse pattern data, dense matrix support, and solver adapter boundary; `step4.md` is complete and extracted the Abaqus parser into IO; `step5.md` is complete and extracted Results/reference comparison code; `step6.md` extracts MITC4 geometry/strain helpers; `step7.md` extracts MITC4 material/stiffness helpers; `step8.md` extracts Assembly and Analysis workflow; `step9.md` is the independent architecture evaluator closeout.
|
||||
- Completed phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||
- Historical execution command: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
||||
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step4.md` is complete and strengthened validation/singular diagnostics; `step5.md` is complete and revalidated the DofManager/reaction foundation; `step6.md` is complete and revalidated the minimum result model plus displacement CSV comparator; `step7.md` is complete and revalidated MITC4 natural coordinates, tying points, center directors, and integration bases; `step8.md` is complete and revalidated degenerated-continuum displacement, direct covariant strain rows, and MITC shear tying rows; `step9.md` is complete and revalidated plane-stress material, convected-to-local transform, and `2 x 2 x 2` material integration scaffolding; `step10.md` is complete and revalidated MITC4 stiffness, internal force, six-DOF transform, and drilling stabilization; `step11.md` is complete and added MITC4 membrane, bending, shear, twist, drilling-sensitivity, and thin-cantilever locking-sensitivity tests; `step12.md` is complete and revalidated full-space assembly, reduced projection, deterministic sparse-pattern scaffold, solver adapter injection, and full-vector internal/reaction force state; `step13.md` is complete and revalidated active AnalysisModel construction plus input-to-AnalysisState-to-U/RF result workflow; `step14.md` is complete and added the first stored Abaqus displacement regression for `quad_02_phase1`; `step15.md` is complete and recorded the independent evaluator closeout in `phases/1-linear-static-mitc4-rebaseline/step15-evaluator-report.md`.
|
||||
@@ -62,7 +62,7 @@ This phase is an architecture-preserving refactor. It must not change Phase 1 so
|
||||
| P1A-02 | completed | generator | Extract Core/Util domain, diagnostics, aliases, DOF mapping, `AnalysisModel`, `DofManager`, and Phase 1 Boundary/Load/Property model ownership. | P1A-01 | Core has no dependency on higher layers; Boundary/Load/Property types are no longer hidden in the umbrella header; DOF tests unchanged |
|
||||
| P1A-03 | completed | generator | Extract Math and solver adapter boundaries. | P1A-02 | Linear solver interface remains adapter-ready; int64 paths unchanged |
|
||||
| P1A-04 | completed | generator | Extract Abaqus parser into IO. | P1A-02 | Parser subset and unsupported-feature diagnostics unchanged |
|
||||
| P1A-05 | pending | generator | Extract Results model, writer boundary, CSV loader, and reference comparator. | P1A-02, P1A-04 | `U`/`RF` schema and `quad_02_phase1` regression unchanged |
|
||||
| P1A-05 | completed | generator | Extract Results model, writer boundary, CSV loader, and reference comparator. | P1A-02, P1A-04 | `U`/`RF` schema and `quad_02_phase1` regression unchanged |
|
||||
| P1A-06 | pending | generator | Extract MITC4 geometry, director, strain, and tying helpers into Element. | P1A-03 | Geometry/strain tests and formulation signs unchanged |
|
||||
| P1A-07 | pending | generator | Extract MITC4 material, integration, stiffness, drilling, and internal-force helpers. | P1A-06 | Patch, drilling, stiffness, and locking-sensitivity tests unchanged |
|
||||
| P1A-08 | pending | generator | Extract Assembly and Analysis workflow. | P1A-02, P1A-03, P1A-05, P1A-07 | Full-vector RF, solver injection, and end-to-end reference regression unchanged |
|
||||
|
||||
+35
-1
@@ -13,10 +13,44 @@ Every new agent session must read this file together with `PLAN.md` before plann
|
||||
- Do not remove history unless the user explicitly asks for archival cleanup.
|
||||
|
||||
## Current Status
|
||||
Phase 1 has a completed rebaseline execution path in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 15 are complete, and P1R-15 recorded a pass-with-documented-gaps evaluator closeout. The follow-up architecture refactor phase in `phases/1-structure-alignment-refactor` is underway because the current production implementation is concentrated in `include/fesa/fesa.hpp` instead of the module directories documented in `docs/ARCHITECTURE.md`; P1A-00 through P1A-04 are complete, so the next step is P1A-05 Results/reference extraction. `quad_02_phase1.inp` is the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, MITC4 displacement/strain/tying row scaffolding, MITC4 material/transform/integration scaffolding, MITC4 stiffness/drilling/internal-force scaffolding, MITC4 patch/locking-sensitivity tests, full-space assembly, reduced projection, sparse-pattern scaffold, solver adapter injection, full-vector internal/reaction force state, active AnalysisModel construction, input-to-AnalysisState-to-U/RF result workflow, and the first stored Abaqus displacement regression have been revalidated. Full PRD Phase 1 completion still depends on the open architecture/reference gaps R-014, R-010, and R-013. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
Phase 1 has a completed rebaseline execution path in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 15 are complete, and P1R-15 recorded a pass-with-documented-gaps evaluator closeout. The follow-up architecture refactor phase in `phases/1-structure-alignment-refactor` is underway because the current production implementation is concentrated in `include/fesa/fesa.hpp` instead of the module directories documented in `docs/ARCHITECTURE.md`; P1A-00 through P1A-05 are complete, so the next step is P1A-06 MITC4 geometry/strain extraction. `quad_02_phase1.inp` is the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, the Phase 1 parser/domain subset, validation/singular diagnostics, DofManager/reaction foundation, minimum result model metadata, displacement CSV comparator foundation, MITC4 geometry/director scaffolding, MITC4 displacement/strain/tying row scaffolding, MITC4 material/transform/integration scaffolding, MITC4 stiffness/drilling/internal-force scaffolding, MITC4 patch/locking-sensitivity tests, full-space assembly, reduced projection, sparse-pattern scaffold, solver adapter injection, full-vector internal/reaction force state, active AnalysisModel construction, input-to-AnalysisState-to-U/RF result workflow, and the first stored Abaqus displacement regression have been revalidated. Full PRD Phase 1 completion still depends on the open architecture/reference gaps R-014, R-010, and R-013. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
|
||||
## Completed Work
|
||||
|
||||
### 2026-05-05 - P1A-05 Results reference extraction completed
|
||||
Author: Codex
|
||||
|
||||
Changed files:
|
||||
- `CMakeLists.txt`
|
||||
- `include/fesa/Results/ReferenceComparison.hpp`
|
||||
- `include/fesa/Results/ResultModel.hpp`
|
||||
- `include/fesa/Results/Results.hpp`
|
||||
- `include/fesa/fesa.hpp`
|
||||
- `tests/test_results_module_includes.cpp`
|
||||
- `phases/1-structure-alignment-refactor/index.json`
|
||||
- `PLAN.md`
|
||||
- `PROGRESS.md`
|
||||
|
||||
Summary:
|
||||
- Extracted `FieldOutput`, `ResultFrame`, `ResultStep`, `ResultFile`, and `InMemoryResultsWriter` from the umbrella header into `include/fesa/Results/ResultModel.hpp`.
|
||||
- Extracted displacement CSV loading, `ComparisonOptions`, `ComparisonResult`, and `compareDisplacements` into `include/fesa/Results/ReferenceComparison.hpp`.
|
||||
- Updated `include/fesa/Results/Results.hpp` to expose the in-memory result model and reference comparator through the Results module.
|
||||
- Updated `include/fesa/fesa.hpp` to include the Results module facade, preserving existing umbrella consumers and leaving Analysis workflow symbols in place for P1A-08.
|
||||
- Added `fesa_results_module_tests`, a direct Results include smoke/regression test that does not include `fesa/fesa.hpp`.
|
||||
- Preserved mandatory Phase 1 `U`/`RF` metadata, CSV column mapping, node-id-based matching, and the stored `quad_02_displacements.csv` loader path.
|
||||
- No HDF5 dependency, reaction CSV parser, result label change, comparison tolerance change, or analysis execution change was introduced.
|
||||
- Remaining large groups in `fesa.hpp` are Assembly helpers (`buildReducedSparsePattern`, `recoverFullReaction`), MITC4 Element/Material helpers, and Analysis workflow.
|
||||
|
||||
Verification:
|
||||
- First ran `python scripts\validate_workspace.py` after adding the direct Results include test; it failed as expected because `fesa/Results/Results.hpp` did not yet expose result and comparison symbols.
|
||||
- After extraction, `python scripts\validate_workspace.py` configured CMake, built `fesa_core`, `fesa_tests`, `fesa_core_module_tests`, `fesa_math_module_tests`, `fesa_io_module_tests`, and `fesa_results_module_tests`, and ran CTest successfully.
|
||||
- CTest result: 5 test executables passed.
|
||||
|
||||
Follow-up:
|
||||
- Continue with P1A-06 MITC4 geometry/strain extraction.
|
||||
- Keep R-014 open until P1A-09 independently accepts the final architecture alignment.
|
||||
- Keep R-010 open; missing Abaqus reaction CSV artifacts are not solved by this refactor.
|
||||
|
||||
### 2026-05-05 - P1A-04 IO parser extraction completed
|
||||
Author: Codex
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
{ "step": 2, "name": "core-domain-dof-extraction", "status": "completed" },
|
||||
{ "step": 3, "name": "math-solver-extraction", "status": "completed" },
|
||||
{ "step": 4, "name": "io-parser-extraction", "status": "completed" },
|
||||
{ "step": 5, "name": "results-reference-extraction", "status": "pending" },
|
||||
{ "step": 5, "name": "results-reference-extraction", "status": "completed" },
|
||||
{ "step": 6, "name": "mitc4-geometry-strain-extraction", "status": "pending" },
|
||||
{ "step": 7, "name": "mitc4-material-stiffness-extraction", "status": "pending" },
|
||||
{ "step": 8, "name": "assembly-analysis-extraction", "status": "pending" },
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
#include "fesa/Results/Results.hpp"
|
||||
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
void check(bool value, const char* message) {
|
||||
if (!value) {
|
||||
throw std::runtime_error(message);
|
||||
}
|
||||
}
|
||||
|
||||
std::string sourceRoot() {
|
||||
#ifdef FESA_SOURCE_DIR
|
||||
return FESA_SOURCE_DIR;
|
||||
#else
|
||||
return ".";
|
||||
#endif
|
||||
}
|
||||
|
||||
fesa::Domain makeDomain() {
|
||||
fesa::Domain domain;
|
||||
domain.nodes[1] = {1, {0.0, 0.0, 0.0}};
|
||||
domain.nodes[2] = {2, {1.0, 0.0, 0.0}};
|
||||
domain.nodes[3] = {3, {1.0, 1.0, 0.0}};
|
||||
domain.nodes[4] = {4, {0.0, 1.0, 0.0}};
|
||||
domain.elements[10] = {10, fesa::ElementType::MITC4, {1, 2, 3, 4}, "all_elements"};
|
||||
domain.steps.push_back({"Step-Results", "linear_static"});
|
||||
domain.boundary_conditions.push_back({"1", 1, 6, 0.0});
|
||||
return domain;
|
||||
}
|
||||
|
||||
std::string readTextFile(const std::string& path) {
|
||||
std::ifstream input(path);
|
||||
if (!input) {
|
||||
throw std::runtime_error("failed to open " + path);
|
||||
}
|
||||
std::ostringstream buffer;
|
||||
buffer << input.rdbuf();
|
||||
return buffer.str();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const auto domain = makeDomain();
|
||||
const fesa::DofManager dofs(domain);
|
||||
std::vector<fesa::Real> u(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
|
||||
std::vector<fesa::Real> rf(static_cast<std::size_t>(dofs.fullDofCount()), 0.0);
|
||||
u[static_cast<std::size_t>(dofs.fullIndex(2, fesa::Dof::UZ))] = -0.25;
|
||||
rf[static_cast<std::size_t>(dofs.fullIndex(1, fesa::Dof::UZ))] = 3.0;
|
||||
|
||||
fesa::InMemoryResultsWriter writer;
|
||||
writer.writeLinearStatic(domain, dofs, u, rf);
|
||||
const auto& result = writer.result();
|
||||
check(result.schema_name == "FESA_RESULTS", "schema name changed");
|
||||
check(result.schema_version == 1, "schema version changed");
|
||||
check(result.solver_name == "FESA", "solver name changed");
|
||||
check(result.dof_convention == "UX,UY,UZ,RX,RY,RZ", "DOF convention changed");
|
||||
check(result.sign_convention == "Abaqus-compatible", "sign convention changed");
|
||||
check(result.precision == "double", "precision metadata changed");
|
||||
check(result.index_type == "int64", "index metadata changed");
|
||||
check(result.node_ids.size() == domain.nodes.size(), "node model mirror changed");
|
||||
check(result.element_types == std::vector<std::string>({"MITC4"}), "element type label changed");
|
||||
check(result.steps.size() == 1, "result step count changed");
|
||||
check(result.steps[0].name == "Step-Results", "step name changed");
|
||||
check(result.steps[0].frames.size() == 1, "result frame count changed");
|
||||
|
||||
const auto& frame = result.steps[0].frames[0];
|
||||
check(frame.frame_id == 0, "Phase 1 frame id changed");
|
||||
check(frame.increment == 1, "Phase 1 increment changed");
|
||||
check(frame.iteration == 0, "Phase 1 iteration changed");
|
||||
check(frame.converged, "Phase 1 convergence flag changed");
|
||||
check(frame.field_outputs.count("U") == 1, "U field missing");
|
||||
check(frame.field_outputs.count("RF") == 1, "RF field missing");
|
||||
const auto& u_field = frame.field_outputs.at("U");
|
||||
const auto& rf_field = frame.field_outputs.at("RF");
|
||||
check(u_field.position == "NODAL" && u_field.entity_type == "node" && u_field.basis == "GLOBAL",
|
||||
"U field metadata changed");
|
||||
check(rf_field.position == "NODAL" && rf_field.entity_type == "node" && rf_field.basis == "GLOBAL",
|
||||
"RF field metadata changed");
|
||||
check(u_field.component_labels == fesa::displacementComponentLabels(), "U component labels changed");
|
||||
check(rf_field.component_labels == fesa::reactionComponentLabels(), "RF component labels changed");
|
||||
|
||||
const auto required_columns = fesa::displacementCsvRequiredColumns();
|
||||
check(required_columns.size() == 7, "required displacement CSV column count changed");
|
||||
check(required_columns.front() == "Node Label", "CSV node label column changed");
|
||||
check(required_columns.back() == "UR-UR3", "CSV rotation column changed");
|
||||
|
||||
const auto missing_header = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2\n"
|
||||
"1,0,0,0,0,0\n",
|
||||
"missing-header.csv");
|
||||
check(fesa::containsDiagnostic(missing_header.diagnostics, "FESA-CSV-MISSING-COLUMN"),
|
||||
"missing CSV header diagnostic changed");
|
||||
|
||||
const auto duplicate_node = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
|
||||
"1,0,0,0,0,0,0\n"
|
||||
"1,0,0,0,0,0,0\n",
|
||||
"duplicate-node.csv");
|
||||
check(fesa::containsDiagnostic(duplicate_node.diagnostics, "FESA-CSV-DUPLICATE-NODE"),
|
||||
"duplicate CSV node diagnostic changed");
|
||||
|
||||
fesa::CsvDisplacementTable expected;
|
||||
expected.rows[1] = {1, {0, 0, 0, 0, 0, 0}};
|
||||
expected.rows[2] = {2, {0, 0, -0.25, 0, 0, 0}};
|
||||
expected.rows[3] = {3, {0, 0, 0, 0, 0, 0}};
|
||||
expected.rows[4] = {4, {0, 0, 0, 0, 0, 0}};
|
||||
const auto comparison = fesa::compareDisplacements(u_field, expected, {1.0e-12, 1.0e-12, 1.0});
|
||||
check(comparison.pass, "displacement comparator no longer matches by node id");
|
||||
|
||||
const auto quad02 = fesa::loadDisplacementCsv(sourceRoot() + "/references/quad_02_displacements.csv");
|
||||
check(!fesa::hasError(quad02.diagnostics), "quad_02 displacement CSV no longer loads");
|
||||
check(quad02.rows.size() == 121, "quad_02 displacement CSV row count changed");
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user