feat: strengthen results comparator foundation
This commit is contained in:
@@ -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
|
||||
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-06 results model and displacement CSV comparator foundation. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
||||
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-07 MITC4 geometry, node order, tying points, directors, and local bases. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
||||
|
||||
## Required Reading For New Agents
|
||||
1. `AGENTS.md`
|
||||
@@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase
|
||||
## Active Phase Files
|
||||
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||
- Execute with: `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; `step15.md` is the independent evaluator closeout.
|
||||
- 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; `step15.md` is the independent evaluator closeout.
|
||||
- Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements.
|
||||
- Historical phase directory: `phases/1-linear-static-mitc4`
|
||||
- Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase.
|
||||
@@ -75,7 +75,7 @@ Each gate should be satisfied before moving to the next implementation band unle
|
||||
| G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md |
|
||||
| G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output |
|
||||
| G2 - Parser and domain | satisfied | Parser subset revalidated in step 3; validation and singular diagnostics revalidated in step 4. | Parser acceptance/rejection tests, validation negative tests, and validation output |
|
||||
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, model diagnostic context, DofManager, sparse-connectivity inputs, and full-vector reaction formula were revalidated in steps 2 and 5; results and assembly remain for steps 6 and 12. | P1R-02 and P1R-05 validation output |
|
||||
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, model diagnostic context, DofManager, sparse-connectivity inputs, full-vector reaction formula, result model metadata, and displacement CSV comparator were revalidated in steps 2, 5, and 6; assembly remains for step 12. | P1R-02, P1R-05, and P1R-06 validation output |
|
||||
| G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests |
|
||||
| G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output |
|
||||
|
||||
@@ -87,7 +87,7 @@ All milestones are intended to become one or more self-contained sprint contract
|
||||
| P1R-03 | completed | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
|
||||
| P1R-04 | completed | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests |
|
||||
| P1R-05 | completed | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests |
|
||||
| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
|
||||
| P1R-06 | completed | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
|
||||
| P1R-07 | pending | MITC4 generator | Implement MITC4 geometry, node order, tying points, directors, and local bases. | none | Shape/basis/diagnostic tests |
|
||||
| P1R-08 | pending | MITC4 generator | Implement degenerated-continuum displacement, covariant strain rows, and MITC shear tying. | P1R-07 | Finite-difference and tying interpolation tests |
|
||||
| P1R-09 | pending | MITC4 generator | Implement material matrix, transform, and `2 x 2 x 2` integration scaffolding. | P1R-08 | Material/integration tests |
|
||||
|
||||
+28
-1
@@ -13,10 +13,37 @@ 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 new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 5 are complete. `quad_02_phase1.inp` is now 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, and DofManager/reaction foundation have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 6 are complete. `quad_02_phase1.inp` is now 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, and displacement CSV comparator foundation have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
|
||||
## Completed Work
|
||||
|
||||
### 2026-05-04 - P1R-06 results model and displacement CSV comparator completed
|
||||
Author: Codex
|
||||
|
||||
Changed files:
|
||||
- `include/fesa/fesa.hpp`
|
||||
- `tests/test_main.cpp`
|
||||
- `docs/RESULTS_SCHEMA.md`
|
||||
- `docs/VERIFICATION_PLAN.md`
|
||||
- `phases/1-linear-static-mitc4-rebaseline/index.json`
|
||||
- `PLAN.md`
|
||||
- `PROGRESS.md`
|
||||
|
||||
Summary:
|
||||
- Added result model tests for Phase 1 root metadata, frame `0` metadata, and mandatory `U`/`RF` field labels, positions, entity type, and global basis.
|
||||
- Extended the in-memory result model with solver/schema metadata, linear-static frame metadata, nodal field metadata, and mandatory `U`/`RF` descriptions while keeping `S`, `E`, and `SF` optional.
|
||||
- Added string/stream-based Abaqus displacement CSV loading so tests can cover required headers, duplicate node labels, missing/non-numeric node labels, and nonnumeric component values without temporary files.
|
||||
- Strengthened displacement comparison so it matches by node id, verifies exact FESA `U` component labels and nodal/global metadata, rejects duplicate actual node ids, reports missing FESA nodes, and uses combined absolute/relative tolerances.
|
||||
|
||||
Verification:
|
||||
- First ran `python scripts/validate_workspace.py` after adding Step 6 tests; it failed as expected because the result metadata fields and string CSV loader did not exist yet.
|
||||
- After implementation, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully.
|
||||
- CTest result: 1 test executable passed.
|
||||
|
||||
Follow-up:
|
||||
- Continue with P1R-07 MITC4 geometry, node order, tying points, directors, and local bases.
|
||||
- Keep RF reference CSV availability open; current RF verification remains internal full-vector equilibrium until a stored `*_reactions.csv` artifact is provided.
|
||||
|
||||
### 2026-05-04 - P1R-05 DofManager and reaction foundation completed
|
||||
Author: Codex
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@ Attach these attributes to `/`:
|
||||
| `unit_system_note` | string | no | User-provided self-consistent unit note |
|
||||
| `dof_convention` | string | yes | `UX,UY,UZ,RX,RY,RZ` |
|
||||
| `sign_convention` | string | yes | `Abaqus-compatible` |
|
||||
| `precision` | string | yes for Phase 1 in-memory result model | `double` |
|
||||
| `index_type` | string | yes for Phase 1 in-memory result model | `int64` |
|
||||
|
||||
## `/metadata`
|
||||
Recommended datasets or attributes:
|
||||
@@ -189,7 +191,7 @@ The final component labels should be cross-checked with the accepted Abaqus refe
|
||||
|
||||
## Phase 1 Mandatory Outputs
|
||||
The first complete linear static solver path must write:
|
||||
- root metadata attributes: `schema_name`, `schema_version`, `solver_name`, `dof_convention`, `sign_convention`.
|
||||
- root metadata attributes: `schema_name`, `schema_version`, `solver_name`, `dof_convention`, `sign_convention`, `precision`, and `index_type`.
|
||||
- `/model/nodes/ids`
|
||||
- `/model/nodes/coordinates`
|
||||
- `/model/elements/ids`
|
||||
@@ -293,6 +295,8 @@ Required CSV columns:
|
||||
Rules:
|
||||
- CSV files are reference inputs for tests, not the primary FESA result storage format.
|
||||
- The comparator must preserve node-label matching and must not rely on row order alone.
|
||||
- The comparator must require FESA `U` component labels `UX`, `UY`, `UZ`, `RX`, `RY`, `RZ` with `position = NODAL`, `entity_type = node`, and `basis = GLOBAL`.
|
||||
- Duplicate FESA output node ids, duplicate CSV node labels, missing FESA nodes, missing CSV columns, and nonnumeric CSV values are comparison failures.
|
||||
- The comparison report may be stored under `/referenceComparison`.
|
||||
- Reaction CSV, stress CSV, or section force CSV formats must be documented before automated use.
|
||||
|
||||
|
||||
@@ -133,7 +133,8 @@ Rules:
|
||||
- `Node Label` is parsed as int64.
|
||||
- All displacement and rotation values are parsed as `double`.
|
||||
- The comparator must match rows by node id, not by row order alone.
|
||||
- Missing nodes in FESA output, duplicate CSV node labels, missing columns, or nonnumeric values are reference artifact errors.
|
||||
- Missing or nonnumeric `Node Label` values, duplicate CSV node labels, missing columns, or nonnumeric component values are reference artifact errors.
|
||||
- Missing nodes in FESA output, duplicate FESA output node ids, wrong FESA `U` component labels, or wrong nodal/global field metadata are comparison errors.
|
||||
- CSV displacement comparison maps to `/results/steps/<step>/frames/<frame>/fieldOutputs/U`.
|
||||
- If the `.inp` has multiple steps or frames, the case metadata must state which step/frame the CSV represents.
|
||||
|
||||
|
||||
+72
-20
@@ -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, {}});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{ "step": 3, "name": "parser-domain-subset", "status": "completed" },
|
||||
{ "step": 4, "name": "validation-singular-diagnostics", "status": "completed" },
|
||||
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "completed" },
|
||||
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
||||
{ "step": 6, "name": "results-comparator-foundation", "status": "completed" },
|
||||
{ "step": 7, "name": "mitc4-geometry-directors", "status": "pending" },
|
||||
{ "step": 8, "name": "mitc4-covariant-strain-tying", "status": "pending" },
|
||||
{ "step": 9, "name": "mitc4-material-integration", "status": "pending" },
|
||||
|
||||
+117
-4
@@ -652,11 +652,40 @@ FESA_TEST(results_writer_uses_step_frame_fields_for_u_and_rf) {
|
||||
writer.writeLinearStatic(domain, dofs, u, rf);
|
||||
const auto& result = writer.result();
|
||||
FESA_CHECK(result.schema_name == "FESA_RESULTS");
|
||||
FESA_CHECK(result.schema_version == 1);
|
||||
FESA_CHECK(result.solver_name == "FESA");
|
||||
FESA_CHECK(result.dof_convention == "UX,UY,UZ,RX,RY,RZ");
|
||||
FESA_CHECK(result.sign_convention == "Abaqus-compatible");
|
||||
FESA_CHECK(result.precision == "double");
|
||||
FESA_CHECK(result.index_type == "int64");
|
||||
FESA_CHECK(result.steps.size() == 1);
|
||||
FESA_CHECK(result.steps[0].frames[0].field_outputs.count("U") == 1);
|
||||
FESA_CHECK(result.steps[0].frames[0].field_outputs.count("RF") == 1);
|
||||
FESA_CHECK(result.steps[0].frames[0].field_outputs.at("U").component_labels[2] == "UZ");
|
||||
FESA_CHECK(result.steps[0].frames[0].field_outputs.at("RF").component_labels[2] == "RFZ");
|
||||
FESA_CHECK(result.steps[0].name == "Step-1");
|
||||
FESA_CHECK(result.steps[0].frames.size() == 1);
|
||||
const auto& frame = result.steps[0].frames[0];
|
||||
FESA_CHECK(frame.frame_id == 0);
|
||||
FESA_CHECK(frame.increment == 1);
|
||||
FESA_CHECK(frame.iteration == 0);
|
||||
FESA_CHECK(frame.converged);
|
||||
FESA_CHECK_NEAR(frame.step_time, 1.0, 1.0e-15);
|
||||
FESA_CHECK_NEAR(frame.total_time, 1.0, 1.0e-15);
|
||||
FESA_CHECK(frame.field_outputs.count("U") == 1);
|
||||
FESA_CHECK(frame.field_outputs.count("RF") == 1);
|
||||
const auto& u_field = frame.field_outputs.at("U");
|
||||
const auto& rf_field = frame.field_outputs.at("RF");
|
||||
FESA_CHECK(u_field.name == "U");
|
||||
FESA_CHECK(u_field.position == "NODAL");
|
||||
FESA_CHECK(u_field.entity_type == "node");
|
||||
FESA_CHECK(u_field.basis == "GLOBAL");
|
||||
FESA_CHECK(u_field.component_labels == fesa::displacementComponentLabels());
|
||||
FESA_CHECK(rf_field.name == "RF");
|
||||
FESA_CHECK(rf_field.position == "NODAL");
|
||||
FESA_CHECK(rf_field.entity_type == "node");
|
||||
FESA_CHECK(rf_field.basis == "GLOBAL");
|
||||
FESA_CHECK(rf_field.component_labels == fesa::reactionComponentLabels());
|
||||
FESA_CHECK(u_field.component_labels[2] == "UZ");
|
||||
FESA_CHECK(rf_field.component_labels[2] == "RFZ");
|
||||
FESA_CHECK(u_field.entity_ids.size() == domain.nodes.size());
|
||||
FESA_CHECK(rf_field.entity_ids.size() == domain.nodes.size());
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_csv_loader_accepts_quad01_format) {
|
||||
@@ -674,9 +703,37 @@ FESA_TEST(displacement_csv_loader_accepts_quad02_format) {
|
||||
FESA_CHECK(table.rows.at(2).values[2] < 0.0);
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_csv_loader_reports_required_header_errors) {
|
||||
auto table = 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");
|
||||
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-MISSING-COLUMN"));
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_csv_loader_reports_duplicate_node_rows) {
|
||||
auto table = 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");
|
||||
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-DUPLICATE-NODE"));
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_csv_loader_reports_missing_and_non_numeric_node_rows) {
|
||||
auto table = fesa::loadDisplacementCsvFromString("Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
|
||||
",0,0,0,0,0,0\n"
|
||||
"two,0,0,0,0,0,0\n"
|
||||
"3,0,not-a-number,0,0,0,0\n",
|
||||
"invalid-node-row.csv");
|
||||
FESA_CHECK(diagnosticCount(table.diagnostics, "FESA-CSV-NODE") == 2);
|
||||
FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-NUMERIC"));
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_comparator_matches_by_node_id_not_row_order) {
|
||||
fesa::FieldOutput actual;
|
||||
actual.name = "U";
|
||||
actual.position = "NODAL";
|
||||
actual.entity_type = "node";
|
||||
actual.basis = "GLOBAL";
|
||||
actual.entity_ids = {2, 1};
|
||||
actual.component_labels = fesa::displacementComponentLabels();
|
||||
actual.values = {{{2, 0, 0, 0, 0, 0}}, {{1, 0, 0, 0, 0, 0}}};
|
||||
@@ -687,6 +744,62 @@ FESA_TEST(displacement_comparator_matches_by_node_id_not_row_order) {
|
||||
FESA_CHECK(compared.pass);
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_comparator_uses_absolute_and_relative_tolerances) {
|
||||
fesa::FieldOutput actual;
|
||||
actual.name = "U";
|
||||
actual.position = "NODAL";
|
||||
actual.entity_type = "node";
|
||||
actual.basis = "GLOBAL";
|
||||
actual.entity_ids = {10};
|
||||
actual.component_labels = fesa::displacementComponentLabels();
|
||||
actual.values = {{{5.0e-7, 100.0005, 0, 0, 0, 0}}};
|
||||
fesa::CsvDisplacementTable expected;
|
||||
expected.rows[10] = {10, {0.0, 100.0, 0, 0, 0, 0}};
|
||||
|
||||
auto loose = fesa::compareDisplacements(actual, expected, {1.0e-6, 1.0e-5, 1.0});
|
||||
FESA_CHECK(loose.pass);
|
||||
FESA_CHECK_NEAR(loose.max_abs_error, 5.0e-4, 1.0e-12);
|
||||
|
||||
auto strict = fesa::compareDisplacements(actual, expected, {1.0e-8, 1.0e-8, 1.0});
|
||||
FESA_CHECK(!strict.pass);
|
||||
FESA_CHECK(fesa::containsDiagnostic(strict.diagnostics, "FESA-COMPARE-TOLERANCE"));
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_comparator_rejects_wrong_component_labels_and_missing_nodes) {
|
||||
fesa::FieldOutput actual;
|
||||
actual.name = "U";
|
||||
actual.position = "NODAL";
|
||||
actual.entity_type = "node";
|
||||
actual.basis = "GLOBAL";
|
||||
actual.entity_ids = {1};
|
||||
actual.component_labels = fesa::reactionComponentLabels();
|
||||
actual.values = {{{0, 0, 0, 0, 0, 0}}};
|
||||
fesa::CsvDisplacementTable expected;
|
||||
expected.rows[2] = {2, {0, 0, 0, 0, 0, 0}};
|
||||
|
||||
auto compared = fesa::compareDisplacements(actual, expected, {1.0e-12, 1.0e-12, 1.0});
|
||||
FESA_CHECK(!compared.pass);
|
||||
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-COMPONENT-LABELS"));
|
||||
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-MISSING-ACTUAL"));
|
||||
}
|
||||
|
||||
FESA_TEST(displacement_comparator_reports_duplicate_actual_nodes) {
|
||||
fesa::FieldOutput actual;
|
||||
actual.name = "U";
|
||||
actual.position = "NODAL";
|
||||
actual.entity_type = "node";
|
||||
actual.basis = "GLOBAL";
|
||||
actual.entity_ids = {1, 1};
|
||||
actual.component_labels = fesa::displacementComponentLabels();
|
||||
actual.values = {{{0, 0, 0, 0, 0, 0}}, {{0, 0, 0, 0, 0, 0}}};
|
||||
fesa::CsvDisplacementTable expected;
|
||||
expected.rows[1] = {1, {0, 0, 0, 0, 0, 0}};
|
||||
|
||||
auto compared = fesa::compareDisplacements(actual, expected, {1.0e-12, 1.0e-12, 1.0});
|
||||
FESA_CHECK(!compared.pass);
|
||||
FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-DUPLICATE-ACTUAL"));
|
||||
}
|
||||
|
||||
FESA_TEST(mitc4_shape_functions_and_stiffness_baseline) {
|
||||
auto shape = fesa::shapeFunctions(0.25, -0.5);
|
||||
const fesa::Real sum = shape.n[0] + shape.n[1] + shape.n[2] + shape.n[3];
|
||||
|
||||
Reference in New Issue
Block a user