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
+4 -4
View File
@@ -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. - If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
## Current Objective ## 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 ## Required Reading For New Agents
1. `AGENTS.md` 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 Files
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline` - Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
- Execute with: `python scripts/execute.py 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. - 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 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. - 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 | | 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 | | 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 | | 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 | | 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 | | 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-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-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-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-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-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 | | P1R-09 | pending | MITC4 generator | Implement material matrix, transform, and `2 x 2 x 2` integration scaffolding. | P1R-08 | Material/integration tests |
+28 -1
View File
@@ -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. - Do not remove history unless the user explicitly asks for archival cleanup.
## Current Status ## 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 ## 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 ### 2026-05-04 - P1R-05 DofManager and reaction foundation completed
Author: Codex Author: Codex
+5 -1
View File
@@ -64,6 +64,8 @@ Attach these attributes to `/`:
| `unit_system_note` | string | no | User-provided self-consistent unit note | | `unit_system_note` | string | no | User-provided self-consistent unit note |
| `dof_convention` | string | yes | `UX,UY,UZ,RX,RY,RZ` | | `dof_convention` | string | yes | `UX,UY,UZ,RX,RY,RZ` |
| `sign_convention` | string | yes | `Abaqus-compatible` | | `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` ## `/metadata`
Recommended datasets or attributes: 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 ## Phase 1 Mandatory Outputs
The first complete linear static solver path must write: 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/ids`
- `/model/nodes/coordinates` - `/model/nodes/coordinates`
- `/model/elements/ids` - `/model/elements/ids`
@@ -293,6 +295,8 @@ Required CSV columns:
Rules: Rules:
- CSV files are reference inputs for tests, not the primary FESA result storage format. - 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 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`. - The comparison report may be stored under `/referenceComparison`.
- Reaction CSV, stress CSV, or section force CSV formats must be documented before automated use. - Reaction CSV, stress CSV, or section force CSV formats must be documented before automated use.
+2 -1
View File
@@ -133,7 +133,8 @@ Rules:
- `Node Label` is parsed as int64. - `Node Label` is parsed as int64.
- All displacement and rotation values are parsed as `double`. - All displacement and rotation values are parsed as `double`.
- The comparator must match rows by node id, not by row order alone. - 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`. - 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. - If the `.inp` has multiple steps or frames, the case metadata must state which step/frame the CSV represents.
+72 -20
View File
@@ -1463,6 +1463,10 @@ inline AssemblyResult assembleSystem(const Domain& domain, const DofManager& dof
struct FieldOutput { struct FieldOutput {
std::string name; 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<GlobalId> entity_ids;
std::vector<std::string> component_labels; std::vector<std::string> component_labels;
std::vector<std::array<Real, 6>> values; std::vector<std::array<Real, 6>> values;
@@ -1470,8 +1474,12 @@ struct FieldOutput {
struct ResultFrame { struct ResultFrame {
LocalIndex frame_id = 0; LocalIndex frame_id = 0;
LocalIndex increment = 1;
LocalIndex iteration = 0;
Real step_time = 1.0; Real step_time = 1.0;
Real total_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; std::map<std::string, FieldOutput> field_outputs;
}; };
@@ -1483,6 +1491,11 @@ struct ResultStep {
struct ResultFile { struct ResultFile {
std::string schema_name = "FESA_RESULTS"; std::string schema_name = "FESA_RESULTS";
LocalIndex schema_version = 1; 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<GlobalId> node_ids;
std::vector<Vec3> coordinates; std::vector<Vec3> coordinates;
std::vector<GlobalId> element_ids; std::vector<GlobalId> element_ids;
@@ -1506,8 +1519,8 @@ class InMemoryResultsWriter {
step.name = domain.steps.empty() ? "Step-1" : domain.steps.front().name; step.name = domain.steps.empty() ? "Step-1" : domain.steps.front().name;
ResultFrame frame; ResultFrame frame;
frame.frame_id = 0; frame.frame_id = 0;
frame.field_outputs["U"] = buildNodalField("U", displacementComponentLabels(), domain, dofs, u_full); frame.field_outputs["U"] = buildNodalField("U", displacementComponentLabels(), "Nodal displacement and rotation", domain, dofs, u_full);
frame.field_outputs["RF"] = buildNodalField("RF", reactionComponentLabels(), domain, dofs, rf_full); frame.field_outputs["RF"] = buildNodalField("RF", reactionComponentLabels(), "Nodal reaction force and moment", domain, dofs, rf_full);
step.frames.push_back(frame); step.frames.push_back(frame);
result_.steps.push_back(step); result_.steps.push_back(step);
} }
@@ -1517,10 +1530,14 @@ class InMemoryResultsWriter {
} }
private: private:
static FieldOutput buildNodalField(const std::string& name, const std::vector<std::string>& labels, const Domain& domain, static FieldOutput buildNodalField(const std::string& name, const std::vector<std::string>& labels, const std::string& description,
const DofManager& dofs, const std::vector<Real>& full_values) { const Domain& domain, const DofManager& dofs, const std::vector<Real>& full_values) {
FieldOutput field; FieldOutput field;
field.name = name; field.name = name;
field.position = "NODAL";
field.entity_type = "node";
field.basis = "GLOBAL";
field.description = description;
field.component_labels = labels; field.component_labels = labels;
for (const auto& [node_id, node] : domain.nodes) { for (const auto& [node_id, node] : domain.nodes) {
(void)node; (void)node;
@@ -1625,19 +1642,18 @@ struct CsvDisplacementTable {
std::vector<Diagnostic> diagnostics; 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; 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; std::string line;
if (!std::getline(input, 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; 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::vector<std::string> headers = splitCsv(line);
std::map<std::string, std::size_t> column; std::map<std::string, std::size_t> column;
for (std::size_t i = 0; i < headers.size(); ++i) { 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) { for (const std::string& name : required) {
if (column.count(name) == 0) { 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)) { if (hasError(table.diagnostics)) {
@@ -1664,11 +1680,11 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
}; };
auto node_id = parseInt64(get("Node Label")); auto node_id = parseInt64(get("Node Label"));
if (!node_id) { 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; continue;
} }
if (table.rows.count(*node_id) != 0) { 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; continue;
} }
CsvDisplacementRow row; CsvDisplacementRow row;
@@ -1676,7 +1692,7 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
for (std::size_t i = 0; i < 6; ++i) { for (std::size_t i = 0; i < 6; ++i) {
auto value = parseReal(get(required[i + 1])); auto value = parseReal(get(required[i + 1]));
if (!value) { 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; value = 0.0;
} }
row.values[i] = *value; row.values[i] = *value;
@@ -1686,6 +1702,21 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) {
return table; 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 { struct ComparisonOptions {
Real abs_tol = 1.0e-12; Real abs_tol = 1.0e-12;
Real rel_tol = 1.0e-5; Real rel_tol = 1.0e-5;
@@ -1705,25 +1736,46 @@ inline ComparisonResult compareDisplacements(const FieldOutput& actual, const Cs
if (hasError(result.diagnostics)) { if (hasError(result.diagnostics)) {
return result; 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; 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]; actual_by_node[actual.entity_ids[i]] = actual.values[i];
} }
for (const auto& [node_id, row] : expected.rows) { for (const auto& [node_id, row] : expected.rows) {
auto actual_it = actual_by_node.find(node_id); auto actual_it = actual_by_node.find(node_id);
if (actual_it == actual_by_node.end()) { 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; continue;
} }
for (std::size_t component = 0; component < 6; ++component) { for (std::size_t component = 0; component < 6; ++component) {
const Real expected_value = row.values[component]; const Real expected_value = row.values[component];
const Real actual_value = actual_it->second[component]; const Real actual_value = actual_it->second[component];
const Real abs_error = std::fabs(actual_value - expected_value); 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_abs_error = std::max(result.max_abs_error, abs_error);
result.max_rel_error = std::max(result.max_rel_error, rel_error); result.max_rel_error = std::max(result.max_rel_error, rel_error);
if (!(abs_error <= options.abs_tol || rel_error <= options.rel_tol)) { 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": 3, "name": "parser-domain-subset", "status": "completed" },
{ "step": 4, "name": "validation-singular-diagnostics", "status": "completed" }, { "step": 4, "name": "validation-singular-diagnostics", "status": "completed" },
{ "step": 5, "name": "dof-manager-reaction-foundation", "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": 7, "name": "mitc4-geometry-directors", "status": "pending" },
{ "step": 8, "name": "mitc4-covariant-strain-tying", "status": "pending" }, { "step": 8, "name": "mitc4-covariant-strain-tying", "status": "pending" },
{ "step": 9, "name": "mitc4-material-integration", "status": "pending" }, { "step": 9, "name": "mitc4-material-integration", "status": "pending" },
+117 -4
View File
@@ -652,11 +652,40 @@ FESA_TEST(results_writer_uses_step_frame_fields_for_u_and_rf) {
writer.writeLinearStatic(domain, dofs, u, rf); writer.writeLinearStatic(domain, dofs, u, rf);
const auto& result = writer.result(); const auto& result = writer.result();
FESA_CHECK(result.schema_name == "FESA_RESULTS"); 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.size() == 1);
FESA_CHECK(result.steps[0].frames[0].field_outputs.count("U") == 1); FESA_CHECK(result.steps[0].name == "Step-1");
FESA_CHECK(result.steps[0].frames[0].field_outputs.count("RF") == 1); FESA_CHECK(result.steps[0].frames.size() == 1);
FESA_CHECK(result.steps[0].frames[0].field_outputs.at("U").component_labels[2] == "UZ"); const auto& frame = result.steps[0].frames[0];
FESA_CHECK(result.steps[0].frames[0].field_outputs.at("RF").component_labels[2] == "RFZ"); 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) { 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_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_TEST(displacement_comparator_matches_by_node_id_not_row_order) {
fesa::FieldOutput actual; fesa::FieldOutput actual;
actual.name = "U"; actual.name = "U";
actual.position = "NODAL";
actual.entity_type = "node";
actual.basis = "GLOBAL";
actual.entity_ids = {2, 1}; actual.entity_ids = {2, 1};
actual.component_labels = fesa::displacementComponentLabels(); actual.component_labels = fesa::displacementComponentLabels();
actual.values = {{{2, 0, 0, 0, 0, 0}}, {{1, 0, 0, 0, 0, 0}}}; 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_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) { FESA_TEST(mitc4_shape_functions_and_stiffness_baseline) {
auto shape = fesa::shapeFunctions(0.25, -0.5); auto shape = fesa::shapeFunctions(0.25, -0.5);
const fesa::Real sum = shape.n[0] + shape.n[1] + shape.n[2] + shape.n[3]; const fesa::Real sum = shape.n[0] + shape.n[1] + shape.n[2] + shape.n[3];