From c47557885d1772b10f883305fc2c2d7c3a274af7 Mon Sep 17 00:00:00 2001 From: NINI Date: Tue, 5 May 2026 23:56:27 +0900 Subject: [PATCH] test: onboard quad02 reaction reference --- AGENTS.md | 1 + PLAN.md | 11 +- PROGRESS.md | 44 +++++- docs/ABAQUS_INPUT_SUBSET.md | 1 + docs/MITC4_FORMULATION.md | 4 +- docs/NUMERICAL_CONVENTIONS.md | 21 +++ docs/README.md | 4 +- docs/RESULTS_SCHEMA.md | 34 ++++- docs/VERIFICATION_PLAN.md | 51 ++++++- include/fesa/Results/ReferenceComparison.hpp | 149 +++++++++++++++++++ references/README.md | 26 +++- references/quad_02_notes.md | 39 ++++- references/quad_02_reactionforces.csv | 122 +++++++++++++++ tests/test_main.cpp | 97 +++++++++++- tests/test_results_module_includes.cpp | 17 +++ 15 files changed, 597 insertions(+), 24 deletions(-) create mode 100644 references/quad_02_reactionforces.csv diff --git a/AGENTS.md b/AGENTS.md index ea47b6a..c1db723 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -67,6 +67,7 @@ - 기준이 되는 Reference 모델들의 해석결과와 비교로 검증 수행 - Abaqus는 실행하지 않는 전제이다. 사용자가 `references/` 아래에 정리한 입력/결과 artifact를 기준으로 비교할 것 - Reference displacement CSV는 Abaqus export column `Node Label`, `U-U1`, `U-U2`, `U-U3`, `UR-UR1`, `UR-UR2`, `UR-UR3`를 FESA `U` field의 `UX`, `UY`, `UZ`, `RX`, `RY`, `RZ`와 비교하는 기본 형식으로 취급할 것 +- Reference reaction CSV는 Abaqus export column `Node Label`, `RF-RF1`, `RF-RF2`, `RF-RF3`, `RM-RM1`, `RM-RM2`, `RM-RM3`를 FESA `RF` field의 `RFX`, `RFY`, `RFZ`, `RMX`, `RMY`, `RMZ`와 비교하는 기본 형식으로 취급할 것. `*_reactionforces.csv`와 `*_reactions.csv`는 모두 반력 reference artifact 후보로 취급하되, 자동 pass gate로 쓰기 전에 case별 tolerance와 현재 mismatch 여부를 문서화할 것 - Reference 비교는 absolute tolerance와 relative tolerance를 함께 사용할 것 ## 명령어 diff --git a/PLAN.md b/PLAN.md index 14313e9..226f3ec 100644 --- a/PLAN.md +++ b/PLAN.md @@ -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 -The Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor` is complete. P1A-09 independently accepted the final module alignment: `include/fesa/fesa.hpp` is now an include-only facade, production symbols are separated under module ownership, validation passes, and R-014 is closed. The next Phase 1 readiness focus is product-level reference verification: resolve R-010 for reaction-force artifact policy/comparison and R-013 for the PRD target of three stored Phase 1 reference cases. +The Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refactor` is complete. P1A-09 independently accepted the final module alignment: `include/fesa/fesa.hpp` is now an include-only facade, production symbols are separated under module ownership, validation passes, and R-014 is closed. The current Phase 1 readiness focus is product-level reference verification: `quad_02_reactionforces.csv` has been onboarded and wired into an RF comparator, but R-010 remains open because the current node-wise FESA RF comparison against Abaqus does not pass. R-013 also remains open for the PRD target of three stored Phase 1 reference cases. ## Required Reading For New Agents 1. `AGENTS.md` @@ -48,7 +48,7 @@ The Phase 1 structure-alignment refactor in `phases/1-structure-alignment-refact ## Phase 1 Readiness Tasks | ID | Status | Owner | Task | Source | |---|---|---|---|---| -| R-010 | pending | user + verification agent | Add or define reaction-force reference artifacts, preferably `*_reactions.csv`, or decide that Phase 1 `RF` is verified by equilibrium tests until Abaqus RF CSV is available. | `docs/VERIFICATION_PLAN.md`, `docs/RESULTS_SCHEMA.md` | +| R-010 | pending | verification + solver agent | Triage the onboarded `references/quad_02_reactionforces.csv` node-wise RF mismatch. The artifact schema and comparator are defined, but `quad_02_phase1` currently does not pass Abaqus RF/RM comparison; do not relax tolerances to close this. | `docs/VERIFICATION_PLAN.md`, `docs/RESULTS_SCHEMA.md`, `references/quad_02_notes.md` | | R-013 | pending | user + verification agent | Add enough additional small Abaqus S4 reference cases for the PRD target of three stored Phase 1 references: one single-element case, one simple multi-element plate/shell case, and one curved shell benchmark. | `docs/PRD.md`, `docs/VERIFICATION_PLAN.md` | ## Phase 1 Structure Alignment Refactor @@ -147,11 +147,12 @@ Current reference state: - `references/quad_01.inp` and `references/quad_01_displacements.csv` are accepted stored artifacts. - `quad_01.inp` contains `S4R`, `Part/Assembly/Instance`, `*Density`, and `NLGEOM=YES`; it is not a Phase 1 parser acceptance case as-is. - `references/quad_02.inp` and `references/quad_02_displacements.csv` have been added by the user as an S4 reference pair. +- `references/quad_02_reactionforces.csv` has been added by the user as the paired Abaqus RF/RM result artifact for `quad_02`. - `quad_02.inp` uses `TYPE=S4`, but also includes `Part/Assembly/Instance`; this is a compatibility decision point, not automatic parser scope expansion. - `references/quad_02_phase1.inp` is the accepted normalized Phase 1-compatible derivative input for the `quad_02` S4 reference pair. Required reference additions or decisions: -- Onboard any provided `*_reactionforces.csv` or `*_reactions.csv` artifact with a documented schema/tolerance, or explicitly use internal equilibrium tests for Phase 1 `RF` until Abaqus RF CSV is accepted. +- Explain or fix the current `quad_02_phase1` node-wise RF mismatch against `quad_02_reactionforces.csv`. Current observed comparison with `abs_tol = 1.0e-6`, `rel_tol = 1.0e-5`, `reference_scale = 1.0` has max absolute error about `612.751347` and max relative error about `0.494032`. - Add more small cases until Phase 1 can pass one single-element case, one simple multi-element plate/shell case, and one curved shell benchmark. ## Phase 1 Risk Controls @@ -180,5 +181,5 @@ Required reference additions or decisions: | C-004 | pending | user + Codex | Confirm that the `fesa-commands` repo plugin appears in the active Codex plugin/command surface after marketplace registration. | `plugins/fesa-commands/`, `.agents/plugins/marketplace.json` | ## Open Questions -- Which Abaqus version will generate reference artifacts? -- Will Phase 1 `RF` be checked from Abaqus reaction CSV or from internal equilibrium tests first? +- Which Abaqus version will generate future reference artifacts when it is not recorded in the input or notes? +- Is the current `quad_02` RF mismatch due to MITC4 formulation details, Abaqus S4 reaction recovery conventions, normalized input differences, or another solver issue? diff --git a/PROGRESS.md b/PROGRESS.md index 5af8332..eb09da9 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -13,10 +13,48 @@ 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 also complete; P1A-09 recorded a passing architecture evaluator closeout, `include/fesa/fesa.hpp` is now an include-only facade, production symbols are separated under module ownership, and R-014 is closed. `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 reference gaps 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 also complete; P1A-09 recorded a passing architecture evaluator closeout, `include/fesa/fesa.hpp` is now an include-only facade, production symbols are separated under module ownership, and R-014 is closed. `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. `quad_02_reactionforces.csv` is now onboarded as the paired Abaqus RF/RM artifact and the Results module can load and compare reaction CSV files, but the current node-wise RF comparison does not pass and R-010 remains open for triage. 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 and reaction 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 reference gaps 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 - quad_02 reaction CSV comparison onboarded with mismatch recorded +Author: Codex + +Changed files: +- `AGENTS.md` +- `docs/README.md` +- `docs/NUMERICAL_CONVENTIONS.md` +- `docs/ABAQUS_INPUT_SUBSET.md` +- `docs/VERIFICATION_PLAN.md` +- `docs/RESULTS_SCHEMA.md` +- `docs/MITC4_FORMULATION.md` +- `references/README.md` +- `references/quad_02_notes.md` +- `references/quad_02_reactionforces.csv` +- `include/fesa/Results/ReferenceComparison.hpp` +- `tests/test_results_module_includes.cpp` +- `tests/test_main.cpp` +- `PLAN.md` +- `PROGRESS.md` + +Summary: +- Onboarded `references/quad_02_reactionforces.csv` as the Abaqus RF/RM reference artifact paired with the stored `quad_02` S4 case. +- Added reaction CSV schema support for columns `Node Label`, `RF-RF1`, `RF-RF2`, `RF-RF3`, `RM-RM1`, `RM-RM2`, and `RM-RM3`, mapped to FESA `RF` components `RFX`, `RFY`, `RFZ`, `RMX`, `RMY`, and `RMZ`. +- Added `CsvReactionTable`, `loadReactionCsv*`, and `compareReactions` to the Results reference-comparison module. +- Added tests for reaction CSV required columns, duplicate-node diagnostics, node-id-based RF comparison, wrong metadata rejection, and `quad_02_reactionforces.csv` fixture discovery. +- Ran the `quad_02_phase1.inp` analysis path and compared FESA `RF` against the stored Abaqus RF/RM CSV. The comparison currently fails node-wise; this is recorded as a known R-010 verification gap rather than hidden by loose tolerances. +- First observed mismatch: node `1` `RFZ`, expected `6860.0`, actual `6652.459896`. Observed maximum absolute error is about `612.751347`, and maximum relative error is about `0.494032` with `abs_tol = 1.0e-6`, `rel_tol = 1.0e-5`, `reference_scale = 1.0`. +- Kept `quad_02.inp` as unsupported provenance; the executable Phase 1 input remains `quad_02_phase1.inp`. + +Verification: +- First ran `python scripts\validate_workspace.py` after adding reaction comparison tests; it failed as expected because the reaction CSV API did not exist yet. +- After adding the API, `python scripts\validate_workspace.py` exposed the current RF mismatch in the strict node-wise comparison. +- The final validation records the RF mismatch as a known gap test and passes: CMake configured, `fesa_core` and all test executables built, and CTest reported 9 of 9 test executables passed. + +Follow-up: +- Keep R-010 open until the `quad_02` node-wise RF mismatch is explained or fixed. +- Do not tune reaction tolerances or drilling stiffness simply to make this RF case pass. + ### 2026-05-05 - P1A-09 Architecture evaluator closeout completed Author: Codex @@ -1229,7 +1267,7 @@ Verification: - `python scripts/validate_workspace.py` ran, but reported no configured validation commands. ## Known Blockers -- A reaction-force CSV may be present as untracked local reference input, but no reaction-force artifact has been onboarded with documented schema, tolerance, and automated comparison yet. +- `quad_02_reactionforces.csv` is onboarded and comparable, but the current FESA node-wise `RF` result does not pass against Abaqus RF/RM values. - The PRD target of three stored Phase 1 reference cases is not yet satisfied; only `quad_02_phase1` is an active stored displacement regression. - The current initial `quad_01.inp` reference contains `S4R`, `Part/Assembly/Instance`, `*Density`, and `NLGEOM=YES`, so it is not a Phase 1 parser acceptance case as-is. @@ -1237,6 +1275,6 @@ Verification: - Implementation could start from the `quad_01` reference input without accounting for its unsupported Abaqus features. - Implementation could treat the old MITC4 kernel as authoritative even though it conflicts with the revised formulation contract. - Future work could accidentally parse the original `quad_02.inp` instead of the normalized `quad_02_phase1.inp` before parser compatibility is explicitly expanded. -- Reaction output may be wrong if full-space stiffness/load data is not preserved or reconstructed. +- Reaction output may remain Abaqus-incompatible at node level until the `quad_02` RF mismatch is explained. - Large-model support may be weakened if any module narrows ids or sparse indices below int64. - Future source/body hardening could accidentally change behavior if inline module implementations are moved into `.cpp` files without preserving the current characterization and `quad_02_phase1` regression tests. diff --git a/docs/ABAQUS_INPUT_SUBSET.md b/docs/ABAQUS_INPUT_SUBSET.md index 4f2c9e1..04c2fca 100644 --- a/docs/ABAQUS_INPUT_SUBSET.md +++ b/docs/ABAQUS_INPUT_SUBSET.md @@ -62,6 +62,7 @@ Current stored reference notes: - It uses `TYPE=S4R`, `Part`, `Assembly`, `Instance`, `*Density`, and `NLGEOM=YES`, all of which are outside the current Phase 1 parser/solver subset. - Its paired `references/quad_01_displacements.csv` is still valid as a stored displacement reference artifact for future compatibility and regression work. - `references/quad_02.inp` uses `TYPE=S4`, so it targets the Phase 1 MITC4 element formulation, and its paired `references/quad_02_displacements.csv` has the accepted displacement CSV shape. +- `references/quad_02_reactionforces.csv` is a paired Abaqus RF/RM result export for the stored `quad_02` case. It is a reference result artifact and does not change parser scope. - `quad_02.inp` still uses Abaqus/CAE `Part`, `Assembly`, `Instance`, and `*Density`; it is therefore a stored S4 reference artifact and compatibility decision point, not automatic parser acceptance as-is. - `references/quad_02_phase1.inp` is the normalized Phase 1-compatible derivative input for `quad_02`. It preserves node ids, element ids/connectivity, S4 element type, elastic material, shell thickness, fixed boundary nodes, load node, and concentrated load while removing `Part/Assembly/Instance`, `*Density`, restart/output request keywords, and unsupported step metadata. diff --git a/docs/MITC4_FORMULATION.md b/docs/MITC4_FORMULATION.md index b8f7c26..1674477 100644 --- a/docs/MITC4_FORMULATION.md +++ b/docs/MITC4_FORMULATION.md @@ -391,11 +391,13 @@ Stored artifacts currently known: - `references/quad_01_displacements.csv` - `references/quad_02.inp` - `references/quad_02_displacements.csv` +- `references/quad_02_reactionforces.csv` Compatibility notes: - `quad_01.inp` contains `S4R`, `Part/Assembly/Instance`, `*Density`, and `NLGEOM=YES`; it remains future compatibility provenance, not a Phase 1 parser acceptance case. -- `quad_02.inp` contains `TYPE=S4`, which is the correct element target for Phase 1, but it also uses `Part/Assembly/Instance`. The next Phase 1 planning pass must either normalize this input into the Phase 1 flat keyword subset or explicitly add a parser sprint for this Abaqus/CAE structure. Do not silently expand parser support while implementing the element. +- `quad_02.inp` contains `TYPE=S4`, which is the correct element target for Phase 1, but it also uses `Part/Assembly/Instance`. The normalized `quad_02_phase1.inp` remains the executable Phase 1 input path. Do not silently expand parser support while implementing or verifying the element. +- `quad_02_reactionforces.csv` is now available for Abaqus RF/RM comparison. The current node-wise RF comparison does not pass; keep this as a formulation/solver verification gap until the mismatch is explained or fixed. ## Implementation Checklist The next MITC4 implementation pass should proceed in this order: diff --git a/docs/NUMERICAL_CONVENTIONS.md b/docs/NUMERICAL_CONVENTIONS.md index 3550364..b052215 100644 --- a/docs/NUMERICAL_CONVENTIONS.md +++ b/docs/NUMERICAL_CONVENTIONS.md @@ -124,6 +124,27 @@ Rules: - Missing nodes, duplicate node labels, missing columns, or nonnumeric values are reference artifact errors. - CSV comparison uses the same absolute/relative tolerance policy as other reference artifacts. +## Abaqus Reaction CSV Mapping +Reference reaction CSV files map Abaqus exported force and moment columns to FESA `RF` components: + +| CSV Column | FESA Component | Meaning | +|---|---|---| +| `Node Label` | `entity_id` | Node id, stored as int64 | +| `RF-RF1` | `RFX` | Reaction force in global 1/x direction | +| `RF-RF2` | `RFY` | Reaction force in global 2/y direction | +| `RF-RF3` | `RFZ` | Reaction force in global 3/z direction | +| `RM-RM1` | `RMX` | Reaction moment about global 1/x direction | +| `RM-RM2` | `RMY` | Reaction moment about global 2/y direction | +| `RM-RM3` | `RMZ` | Reaction moment about global 3/z direction | + +Rules: +- Accepted reaction CSV filenames may use `*_reactionforces.csv` or `*_reactions.csv`. +- CSV numeric values are parsed as `double`. +- Node labels are matched to FESA result entity ids exactly. +- Missing nodes, duplicate node labels, missing columns, or nonnumeric values are reference artifact errors. +- Node-wise reaction comparison uses the full-vector FESA `RF` field, not reduced solver quantities. +- Do not relax tolerances to pass a reaction comparison without documenting the numerical reason. + ## Boundary Conditions Phase 1 uses constrained DOF elimination: diff --git a/docs/README.md b/docs/README.md index 6421745..a589877 100644 --- a/docs/README.md +++ b/docs/README.md @@ -72,13 +72,15 @@ If a lower-precedence document needs to override a higher-precedence decision, u - Require singular system diagnostics. - Defer mesh quality diagnostics. - Validate against stored reference artifacts under `../references/`; do not require Abaqus execution. -- Treat Abaqus `*.inp` files plus `*_displacements.csv` result files as the initial accepted reference artifact contract. +- Treat Abaqus `*.inp` files plus `*_displacements.csv` result files as the initial accepted displacement reference artifact contract. +- Treat Abaqus `*_reactionforces.csv` or `*_reactions.csv` files as reaction reference artifacts only after their RF/RM column mapping, tolerance, and pass/fail status are documented. ## Implementation Readiness Checklist Before creating Phase 1 implementation steps: - Use `../references/` as the accepted reference artifact folder. - Use Abaqus `.inp` files plus solved `*_displacements.csv` files as the first automated displacement reference format. +- Use Abaqus RF/RM CSV files such as `*_reactionforces.csv` as the first reaction reference format when provided, but do not hide a node-wise RF mismatch by relaxing tolerances. - Keep a compatibility note when a stored Abaqus reference input contains features outside the current Phase 1 parser subset. - Use the current `MITC4_FORMULATION.md` as the Phase 1 MITC4 gate before implementing or reviewing element stiffness. - Treat `U` and `RF` as the mandatory Phase 1 outputs; `S`, `E`, and `SF` require a later recovery-location decision. diff --git a/docs/RESULTS_SCHEMA.md b/docs/RESULTS_SCHEMA.md index ce163ef..c01c3a4 100644 --- a/docs/RESULTS_SCHEMA.md +++ b/docs/RESULTS_SCHEMA.md @@ -298,7 +298,37 @@ Rules: - 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. + +Initial accepted reaction reference naming: + +```text +references/_reactionforces.csv +references/_reactions.csv +``` + +`*_reactionforces.csv` and `*_reactions.csv` map to: + +```text +/results/steps//frames//fieldOutputs/RF +``` + +Required CSV columns: + +| CSV Column | HDF5 Field Component | +|---|---| +| `Node Label` | `entity_ids` | +| `RF-RF1` | `RFX` | +| `RF-RF2` | `RFY` | +| `RF-RF3` | `RFZ` | +| `RM-RM1` | `RMX` | +| `RM-RM2` | `RMY` | +| `RM-RM3` | `RMZ` | + +Rules: +- The comparator must require FESA `RF` component labels `RFX`, `RFY`, `RFZ`, `RMX`, `RMY`, `RMZ` with `position = NODAL`, `entity_type = node`, and `basis = GLOBAL`. +- The FESA `RF` field must be the full-vector reaction field recovered as `K_full * U_full - F_full`. +- Duplicate FESA output node ids, duplicate CSV node labels, missing FESA nodes, missing CSV columns, and nonnumeric CSV values are comparison failures. +- Stress CSV or section force CSV formats must be documented before automated use. ## Naming Rules - Use stable ASCII group and dataset names. @@ -318,4 +348,4 @@ For large models: - Exact mandatory stress/strain/resultant output variables in Phase 1. - Whether result files always mirror the model or only store output entity ids. - Whether reference comparison results are stored in solver output files or separate reports. -- Exact naming and column contracts for non-displacement Abaqus CSV reference files. +- Exact naming and column contracts for stress, strain, and section-force Abaqus CSV reference files. diff --git a/docs/VERIFICATION_PLAN.md b/docs/VERIFICATION_PLAN.md index e3466ae..cfde64a 100644 --- a/docs/VERIFICATION_PLAN.md +++ b/docs/VERIFICATION_PLAN.md @@ -52,6 +52,7 @@ references/ quad_02.inp quad_02_phase1.inp quad_02_displacements.csv + quad_02_reactionforces.csv ``` `quad_01_displacements.csv` contains 121 nodal rows with these columns: @@ -62,6 +63,12 @@ Node Label, U-U1, U-U2, U-U3, UR-UR1, UR-UR2, UR-UR3 `quad_02_displacements.csv` uses the same required columns and contains 121 nodal rows. +`quad_02_reactionforces.csv` contains 121 nodal rows with these columns: + +```text +Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3 +``` + Future manifest-driven layout is still recommended as the case set grows: ```text @@ -140,13 +147,44 @@ Rules: Structured JSON or HDF5 comparison artifacts may be added later, but `*_displacements.csv` is the accepted first automated reference format. +## Reaction CSV Artifact +Initial automated reaction comparison uses Abaqus-exported CSV files named: + +```text +_reactionforces.csv +_reactions.csv +``` + +Required columns: + +| CSV Column | FESA Field | Component | +|---|---|---| +| `Node Label` | `RF` | entity id | +| `RF-RF1` | `RF` | `RFX` | +| `RF-RF2` | `RF` | `RFY` | +| `RF-RF3` | `RF` | `RFZ` | +| `RM-RM1` | `RF` | `RMX` | +| `RM-RM2` | `RF` | `RMY` | +| `RM-RM3` | `RF` | `RMZ` | + +Rules: +- Column matching is by exact normalized header text after trimming whitespace. +- `Node Label` is parsed as int64. +- All reaction force and reaction moment values are parsed as `double`. +- The comparator must match rows by node id, not by row order alone. +- 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 `RF` component labels, or wrong nodal/global field metadata are comparison errors. +- CSV reaction comparison maps to `/results/steps//frames//fieldOutputs/RF`. +- FESA `RF` must be recovered from full vectors, `K_full * U_full - F_full`. + ## Other Result Artifacts Additional Abaqus result exports may be added as the solver grows. Recommended naming: | File Pattern | Purpose | Status | |---|---|---| | `*_displacements.csv` | Nodal `U` displacement/rotation comparison | Accepted initial format | -| `*_reactions.csv` | Nodal `RF` force/moment comparison | Recommended future format | +| `*_reactionforces.csv` | Nodal `RF` force/moment comparison | Accepted for `quad_02`; current node-wise comparison exposes an open solver/reference mismatch | +| `*_reactions.csv` | Nodal `RF` force/moment comparison | Accepted alias for future reaction CSV artifacts | | `*_stresses.csv` | Stress output comparison | Future, after `S` output is documented | | `*_strains.csv` | Strain output comparison | Future, after `E` output is documented | | `*_section_forces.csv` | Shell resultant comparison | Future, after `SF` output is documented | @@ -161,8 +199,8 @@ Current initial case: | Case | Files | Notes | |---|---|---| | `quad_01` | `quad_01.inp`, `quad_01_displacements.csv` | Abaqus/CAE Learning Edition 2024 input; 121 displacement rows; includes `S4R`, `Part/Assembly/Instance`, `*Density`, and `NLGEOM=YES`, which are outside the current Phase 1 parser/solver subset | -| `quad_02` | `quad_02.inp`, `quad_02_displacements.csv` | Abaqus/CAE input; 121 displacement rows; uses `TYPE=S4` and `NLGEOM=NO`, but still includes `Part/Assembly/Instance` and `*Density`, so it must be normalized or handled by an explicit parser compatibility sprint before automated Phase 1 input acceptance | -| `quad_02_phase1` | `quad_02_phase1.inp`, `quad_02_displacements.csv`, `quad_02_notes.md` | Phase 1-compatible derivative input for `quad_02`; preserves ids, connectivity, material, thickness, boundary nodes, load node, and load magnitude while removing unsupported Abaqus/CAE scaffolding | +| `quad_02` | `quad_02.inp`, `quad_02_displacements.csv`, `quad_02_reactionforces.csv` | Abaqus/CAE input; 121 displacement rows and 121 RF/RM rows; uses `TYPE=S4` and `NLGEOM=NO`, but still includes `Part/Assembly/Instance` and `*Density`, so it must be normalized or handled by an explicit parser compatibility sprint before automated Phase 1 input acceptance | +| `quad_02_phase1` | `quad_02_phase1.inp`, `quad_02_displacements.csv`, `quad_02_reactionforces.csv`, `quad_02_notes.md` | Phase 1-compatible derivative input for `quad_02`; preserves ids, connectivity, material, thickness, boundary nodes, load node, and load magnitude while removing unsupported Abaqus/CAE scaffolding | Rules: - Original `.inp` files under `references/` should not be modified just to fit FESA Phase 1. @@ -196,6 +234,7 @@ The first automated stored-reference displacement regression is active for: ```text input: references/quad_02_phase1.inp expected U: references/quad_02_displacements.csv +expected RF: references/quad_02_reactionforces.csv ``` Comparison rules: @@ -203,7 +242,9 @@ Comparison rules: - The normalized `quad_02_phase1.inp` is the executable Phase 1 input for this reference pair. - The FESA `U` field is compared node-id-by-node-id against Abaqus CSV columns `U-U1`, `U-U2`, `U-U3`, `UR-UR1`, `UR-UR2`, and `UR-UR3`. - The active tolerance is `abs_tol = 1.0e-12`, `rel_tol = 1.0e-5`, `reference_scale = 1.0`. -- Abaqus reaction CSV is still unavailable, so `RF` remains verified by full-vector equilibrium tests until a `*_reactions.csv` artifact is provided. +- The FESA `RF` field can now be compared node-id-by-node-id against Abaqus CSV columns `RF-RF1`, `RF-RF2`, `RF-RF3`, `RM-RM1`, `RM-RM2`, and `RM-RM3`. +- Current `quad_02_phase1` node-wise RF comparison is intentionally recorded as a non-passing known gap, not an accepted pass gate: with `abs_tol = 1.0e-6`, `rel_tol = 1.0e-5`, `reference_scale = 1.0`, the observed maximum absolute error is about `612.751347`, maximum relative error is about `0.494032`, and the first observed mismatch is node `1` `RFZ`, expected `6860.0`, actual `6652.459896`. +- Keep R-010 open until the `quad_02` RF mismatch is explained or fixed. Do not loosen reaction tolerances just to make this case pass. ## Phase 1 Benchmark Matrix | Case | Purpose | Required Output | @@ -275,7 +316,7 @@ Required negative tests: ## User Inputs Needed - Additional small Abaqus `.inp` files and solved result CSV files under `references/`. -- Reaction output artifacts when available, preferably a documented `*_reactions.csv`. +- Additional reaction output artifacts when available, preferably documented `*_reactionforces.csv` or `*_reactions.csv` files. - Abaqus version used to generate each reference when it is not evident from the `.inp`. - Unit system notes and tolerances for each case. - Whether future Phase 1-compatible reference files will use Abaqus `S4`, while `S4R` remains deferred. diff --git a/include/fesa/Results/ReferenceComparison.hpp b/include/fesa/Results/ReferenceComparison.hpp index d0382be..4745957 100644 --- a/include/fesa/Results/ReferenceComparison.hpp +++ b/include/fesa/Results/ReferenceComparison.hpp @@ -26,10 +26,24 @@ struct CsvDisplacementTable { std::vector diagnostics; }; +struct CsvReactionRow { + GlobalId node_id = 0; + std::array values{}; +}; + +struct CsvReactionTable { + std::map rows; + std::vector diagnostics; +}; + inline std::vector displacementCsvRequiredColumns() { return {"Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}; } +inline std::vector reactionCsvRequiredColumns() { + return {"Node Label", "RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"}; +} + inline CsvDisplacementTable loadDisplacementCsvFromStream(std::istream& input, const std::string& source_name) { CsvDisplacementTable table; std::string line; @@ -101,6 +115,77 @@ inline CsvDisplacementTable loadDisplacementCsv(const std::string& path) { return loadDisplacementCsvFromStream(input, path); } +inline CsvReactionTable loadReactionCsvFromStream(std::istream& input, const std::string& source_name) { + CsvReactionTable table; + std::string line; + if (!std::getline(input, line)) { + table.diagnostics.push_back({Severity::Error, "FESA-CSV-EMPTY", "Reaction CSV is empty", {source_name, 1, ""}}); + return table; + } + const std::vector required = reactionCsvRequiredColumns(); + std::vector headers = splitCsv(line); + std::map 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 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; + } + CsvReactionRow 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 reaction value", {source_name, line_number, ""}}); + value = 0.0; + } + row.values[i] = *value; + } + table.rows[*node_id] = row; + } + return table; +} + +inline CsvReactionTable loadReactionCsvFromString(const std::string& text, const std::string& source_name = "") { + std::istringstream input(text); + return loadReactionCsvFromStream(input, source_name); +} + +inline CsvReactionTable loadReactionCsv(const std::string& path) { + std::ifstream input(path); + if (!input.good()) { + CsvReactionTable table; + table.diagnostics.push_back({Severity::Error, "FESA-CSV-READ", "Could not read reaction CSV", {path, 0, ""}}); + return table; + } + return loadReactionCsvFromStream(input, path); +} + struct ComparisonOptions { Real abs_tol = 1.0e-12; Real rel_tol = 1.0e-5; @@ -174,4 +259,68 @@ inline ComparisonResult compareDisplacements(const FieldOutput& actual, return result; } +inline ComparisonResult compareReactions(const FieldOutput& actual, + const CsvReactionTable& expected, + ComparisonOptions options = {}) { + ComparisonResult result; + result.diagnostics = expected.diagnostics; + if (hasError(result.diagnostics)) { + return result; + } + if (actual.name != "RF") { + result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-FIELD-NAME", "Expected FESA reaction field named RF", {}}); + } + if (actual.component_labels != reactionComponentLabels()) { + result.diagnostics.push_back({Severity::Error, + "FESA-COMPARE-COMPONENT-LABELS", + "FESA RF field component labels must be RFX,RFY,RFZ,RMX,RMY,RMZ", + {}}); + } + if (actual.position != "NODAL" || actual.entity_type != "node" || actual.basis != "GLOBAL") { + result.diagnostics.push_back({Severity::Error, "FESA-COMPARE-FIELD-METADATA", "FESA RF 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 RF field entity/value counts differ", {}}); + } + std::map> 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 RF 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 RF 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::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 = reactionComponentLabels()[component]; + result.diagnostics.push_back({Severity::Error, + "FESA-COMPARE-TOLERANCE", + "Reaction comparison failed at node " + std::to_string(node_id) + " component " + component_label + + " expected=" + std::to_string(expected_value) + + " actual=" + std::to_string(actual_value) + + " abs_error=" + std::to_string(abs_error) + + " rel_error=" + std::to_string(rel_error), + {}}); + } + } + } + result.pass = !hasError(result.diagnostics); + return result; +} + } // namespace fesa diff --git a/references/README.md b/references/README.md index 4fc9834..e8fb34b 100644 --- a/references/README.md +++ b/references/README.md @@ -9,8 +9,8 @@ Abaqus is not run by the repository validation flow. Files here are treated as s | Case | Input | Result Artifact | Notes | |---|---|---|---| | `quad_01` | `quad_01.inp` | `quad_01_displacements.csv` | Abaqus/CAE Learning Edition 2024 source input; displacement CSV has 121 nodal rows | -| `quad_02` | `quad_02.inp` | `quad_02_displacements.csv` | Abaqus/CAE Learning Edition 2024 source input; `TYPE=S4`; displacement CSV has 121 nodal rows; original input remains unsupported provenance because it contains Abaqus/CAE scaffolding | -| `quad_02_phase1` | `quad_02_phase1.inp` | `quad_02_displacements.csv` | Normalized Phase 1 parser-compatible derivative of `quad_02.inp`; preserves ids, connectivity, material, shell thickness, fixed boundary set, and concentrated load | +| `quad_02` | `quad_02.inp` | `quad_02_displacements.csv`, `quad_02_reactionforces.csv` | Abaqus/CAE Learning Edition 2024 source input; `TYPE=S4`; displacement and RF/RM CSV files each have 121 nodal rows; original input remains unsupported provenance because it contains Abaqus/CAE scaffolding | +| `quad_02_phase1` | `quad_02_phase1.inp` | `quad_02_displacements.csv`, `quad_02_reactionforces.csv` | Normalized Phase 1 parser-compatible derivative of `quad_02.inp`; preserves ids, connectivity, material, shell thickness, fixed boundary set, and concentrated load | Case-specific notes: - `quad_02_notes.md` @@ -35,6 +35,28 @@ Mapping to FESA: | `UR-UR2` | `RY` | | `UR-UR3` | `RZ` | +## Reaction CSV Format + +`*_reactionforces.csv` and `*_reactions.csv` files use Abaqus-exported nodal reaction columns: + +```text +Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3 +``` + +Mapping to FESA: + +| CSV Column | FESA Component | +|---|---| +| `Node Label` | node id | +| `RF-RF1` | `RFX` | +| `RF-RF2` | `RFY` | +| `RF-RF3` | `RFZ` | +| `RM-RM1` | `RMX` | +| `RM-RM2` | `RMY` | +| `RM-RM3` | `RMZ` | + +`quad_02_reactionforces.csv` is onboarded as a stored reaction artifact, but the current FESA node-wise RF comparison does not pass yet. Keep the mismatch visible until the solver/formulation difference is explained or fixed. + ## Compatibility Notes Stored Abaqus inputs may contain features outside the current FESA Phase 1 parser subset. Preserve the original files and document unsupported features instead of editing them in place. diff --git a/references/quad_02_notes.md b/references/quad_02_notes.md index 514dc1f..db61426 100644 --- a/references/quad_02_notes.md +++ b/references/quad_02_notes.md @@ -6,6 +6,7 @@ The original Abaqus input remains preserved as provenance: - `quad_02.inp` - `quad_02_displacements.csv` +- `quad_02_reactionforces.csv` The Phase 1 parser-compatible derivative input is: - `quad_02_phase1.inp` @@ -69,6 +70,29 @@ with component order: UX, UY, UZ, RX, RY, RZ ``` +`quad_02_reactionforces.csv` is an Abaqus-exported nodal reaction force/moment table with 121 rows. + +Required columns: +- `Node Label` +- `RF-RF1` +- `RF-RF2` +- `RF-RF3` +- `RM-RM1` +- `RM-RM2` +- `RM-RM3` + +It maps to FESA field output: + +```text +/results/steps/Step-1/frames/0/fieldOutputs/RF +``` + +with component order: + +```text +RFX, RFY, RFZ, RMX, RMY, RMZ +``` + ## Initial Tolerance The active automated displacement regression uses: @@ -85,6 +109,19 @@ Do not tune tolerances or drilling stiffness to make this single case pass. The regression compares FESA `U` against the stored Abaqus CSV by node id and uses the tolerance above. +`quad_02_phase1.inp` and `quad_02_reactionforces.csv` are also wired through the reaction CSV loader and node-wise `RF` comparator. This comparison currently records a known non-passing gap rather than an accepted pass gate: + +```text +abs_tol = 1.0e-6 +rel_tol = 1.0e-5 +reference_scale = 1.0 +max_abs_error ~= 612.751347 +max_rel_error ~= 0.494032 +first_mismatch = node 1 RFZ, expected 6860.0, actual 6652.459896 +``` + +Do not relax reaction tolerances to make this case pass. Treat the mismatch as a solver/formulation verification item. + ## Current Limitations -- `RF` has no paired Abaqus reaction CSV yet; verify `RF` by full-vector equilibrium until a `quad_02_reactions.csv` artifact is provided. +- The stored Abaqus reaction CSV is available, but node-wise `RF` agreement is not accepted yet because the current comparison fails. - This is currently the only passing stored Abaqus reference regression. The PRD target still requires at least three stored reference models: one single-element case, one simple multi-element plate/shell case, and one curved shell benchmark. diff --git a/references/quad_02_reactionforces.csv b/references/quad_02_reactionforces.csv new file mode 100644 index 0000000..ece4cbe --- /dev/null +++ b/references/quad_02_reactionforces.csv @@ -0,0 +1,122 @@ + Node Label, RF-RF1, RF-RF2, RF-RF3, RM-RM1, RM-RM2, RM-RM3 +1,-8.07E-13,-5.43E-14,6.86E+03,9.09E-13,2.40E+04,8.36E-15 +2,0,0,0,0,0,0 +3,-2.21E-13,6.87E-13,6.86E+03,2.40E+04,-1.82E-12,2.53E-16 +4,-4.31E-14,8.33E-14,-743.226746,490.872711,490.872711,0 +5,2.48E-13,-5.84E-14,6.86E+03,4.09E-12,-2.40E+04,4.70E-15 +6,-1.84E-13,-2.04E-13,-743.226746,490.872711,-490.872711,0 +7,-6.16E-13,-3.42E-13,6.86E+03,-2.40E+04,9.09E-13,-4.89E-15 +8,1.02E-13,-1.15E-13,-743.226746,-490.872711,-490.872711,0 +9,-6.54E-14,-1.07E-13,-743.226746,-490.872711,490.872711,0 +10,0,0,0,0,0,0 +11,0,0,0,0,0,0 +12,0,0,0,0,0,0 +13,0,0,0,0,0,0 +14,0,0,0,0,0,0 +15,0,0,0,0,0,0 +16,0,0,0,0,0,0 +17,0,0,0,0,0,0 +18,-1.36E-13,-1.05E-13,5.97E+03,2.17E+04,819.817993,-7.10E-15 +19,-3.44E-13,8.09E-13,3.67E+03,1.58E+04,1.42E+03,-1.32E-15 +20,2.68E-13,-1.41E-12,749.094788,8.27E+03,1.42E+03,-2.01E-15 +21,2.55E-13,-2.02E-13,-947.392273,2.61E+03,822.967346,-5.79E-16 +22,5.13E-13,-3.77E-13,-947.392273,822.967346,2.61E+03,1.42E-15 +23,-5.10E-13,-2.58E-13,749.094788,1.42E+03,8.27E+03,6.24E-15 +24,-1.15E-13,-2.30E-13,3.67E+03,1.42E+03,1.58E+04,7.89E-15 +25,4.34E-13,5.17E-14,5.97E+03,819.817993,2.17E+04,9.21E-15 +26,0,0,0,0,0,0 +27,0,0,0,0,0,0 +28,0,0,0,0,0,0 +29,0,0,0,0,0,0 +30,-1.14E-13,-1.53E-13,5.97E+03,819.817993,-2.17E+04,-8.24E-15 +31,-1.27E-12,3.44E-13,3.67E+03,1.42E+03,-1.58E+04,-1.06E-14 +32,-1.15E-13,-5.05E-13,749.094788,1.42E+03,-8.27E+03,-1.60E-14 +33,-1.12E-13,-3.07E-14,-947.392273,822.967346,-2.61E+03,-1.26E-14 +34,-4.03E-14,-1.92E-13,-947.392273,2.61E+03,-822.967346,1.18E-14 +35,1.19E-13,2.40E-13,749.094788,8.27E+03,-1.42E+03,1.14E-14 +36,-6.27E-14,8.10E-13,3.67E+03,1.58E+04,-1.42E+03,6.78E-15 +37,6.27E-14,1.81E-13,5.97E+03,2.17E+04,-819.817993,1.68E-15 +38,0,0,0,0,0,0 +39,0,0,0,0,0,0 +40,0,0,0,0,0,0 +41,0,0,0,0,0,0 +42,-5.00E-14,1.36E-12,5.97E+03,-2.17E+04,-819.817993,-5.44E-15 +43,9.99E-14,2.80E-13,3.67E+03,-1.58E+04,-1.42E+03,-9.03E-15 +44,-2.16E-13,8.84E-14,749.094788,-8.27E+03,-1.42E+03,-8.14E-15 +45,-3.92E-13,-7.78E-14,-947.392273,-2.61E+03,-822.967346,-6.83E-15 +46,1.93E-13,2.02E-13,-947.392273,-822.967346,-2.61E+03,-3.86E-15 +47,-9.65E-13,-1.51E-14,749.094788,-1.42E+03,-8.27E+03,3.14E-15 +48,9.48E-13,8.02E-14,3.67E+03,-1.42E+03,-1.58E+04,5.00E-15 +49,-7.88E-13,5.70E-14,5.97E+03,-819.817993,-2.17E+04,5.91E-15 +50,1.65E-13,-4.66E-14,5.97E+03,-819.817993,2.17E+04,1.72E-15 +51,1.08E-13,1.59E-13,3.67E+03,-1.42E+03,1.58E+04,-5.18E-15 +52,7.29E-13,1.81E-13,749.094788,-1.42E+03,8.27E+03,-3.90E-15 +53,-2.53E-13,1.69E-14,-947.392273,-822.967346,2.61E+03,8.11E-15 +54,5.14E-14,-4.00E-14,-947.392273,-2.61E+03,822.967346,-3.97E-15 +55,-6.50E-14,1.41E-13,749.094788,-8.27E+03,1.42E+03,4.21E-15 +56,3.71E-14,-8.15E-14,3.67E+03,-1.58E+04,1.42E+03,1.99E-15 +57,4.68E-13,5.31E-13,5.97E+03,-2.17E+04,819.817993,4.83E-15 +58,0,0,0,0,0,0 +59,0,0,0,0,0,0 +60,0,0,0,0,0,0 +61,0,0,0,0,0,0 +62,0,0,0,0,0,0 +63,0,0,0,0,0,0 +64,0,0,0,0,0,0 +65,0,0,0,0,0,0 +66,0,0,0,0,0,0 +67,0,0,0,0,0,0 +68,0,0,0,0,0,0 +69,0,0,0,0,0,0 +70,0,0,0,0,0,0 +71,0,0,0,0,0,0 +72,0,0,0,0,0,0 +73,0,0,0,0,0,0 +74,0,0,0,0,0,0 +75,0,0,0,0,0,0 +76,0,0,0,0,0,0 +77,0,0,0,0,0,0 +78,0,0,0,0,0,0 +79,0,0,0,0,0,0 +80,0,0,0,0,0,0 +81,0,0,0,0,0,0 +82,0,0,0,0,0,0 +83,0,0,0,0,0,0 +84,0,0,0,0,0,0 +85,0,0,0,0,0,0 +86,0,0,0,0,0,0 +87,0,0,0,0,0,0 +88,0,0,0,0,0,0 +89,0,0,0,0,0,0 +90,0,0,0,0,0,0 +91,0,0,0,0,0,0 +92,0,0,0,0,0,0 +93,0,0,0,0,0,0 +94,0,0,0,0,0,0 +95,0,0,0,0,0,0 +96,0,0,0,0,0,0 +97,0,0,0,0,0,0 +98,0,0,0,0,0,0 +99,0,0,0,0,0,0 +100,0,0,0,0,0,0 +101,0,0,0,0,0,0 +102,0,0,0,0,0,0 +103,0,0,0,0,0,0 +104,0,0,0,0,0,0 +105,0,0,0,0,0,0 +106,0,0,0,0,0,0 +107,0,0,0,0,0,0 +108,0,0,0,0,0,0 +109,0,0,0,0,0,0 +110,0,0,0,0,0,0 +111,0,0,0,0,0,0 +112,0,0,0,0,0,0 +113,0,0,0,0,0,0 +114,0,0,0,0,0,0 +115,0,0,0,0,0,0 +116,0,0,0,0,0,0 +117,0,0,0,0,0,0 +118,0,0,0,0,0,0 +119,0,0,0,0,0,0 +120,0,0,0,0,0,0 +121,0,0,0,0,0,0 diff --git a/tests/test_main.cpp b/tests/test_main.cpp index bedf691..8a2a477 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -213,10 +213,14 @@ std::string readTextFile(const std::string& path) { void checkComparisonPass(const fesa::ComparisonResult& comparison) { if (!comparison.pass) { - throw std::runtime_error("reference comparison failed: max_abs_error=" + - std::to_string(comparison.max_abs_error) + - ", max_rel_error=" + std::to_string(comparison.max_rel_error) + - ", diagnostics=" + std::to_string(comparison.diagnostics.size())); + std::string message = "reference comparison failed: max_abs_error=" + + std::to_string(comparison.max_abs_error) + + ", max_rel_error=" + std::to_string(comparison.max_rel_error) + + ", diagnostics=" + std::to_string(comparison.diagnostics.size()); + if (!comparison.diagnostics.empty()) { + message += ", first_diagnostic=" + comparison.diagnostics.front().message; + } + throw std::runtime_error(message); } } @@ -1009,6 +1013,20 @@ FESA_TEST(displacement_csv_loader_accepts_quad02_format) { FESA_CHECK(table.rows.at(2).values[2] < 0.0); } +FESA_TEST(reaction_csv_loader_accepts_quad02_reactionforces_format) { + const auto required_columns = fesa::reactionCsvRequiredColumns(); + FESA_CHECK(required_columns == std::vector({"Node Label", "RF-RF1", "RF-RF2", "RF-RF3", + "RM-RM1", "RM-RM2", "RM-RM3"})); + + auto table = fesa::loadReactionCsv(sourceRoot() + "/references/quad_02_reactionforces.csv"); + FESA_CHECK(!fesa::hasError(table.diagnostics)); + FESA_CHECK(table.rows.size() == 121); + FESA_CHECK(table.rows.count(1) == 1); + FESA_CHECK(table.rows.count(2) == 1); + FESA_CHECK_NEAR(table.rows.at(1).values[2], 6.86e3, 1.0e-9); + FESA_CHECK_NEAR(table.rows.at(1).values[4], 2.40e4, 1.0e-8); +} + 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", @@ -1016,6 +1034,13 @@ FESA_TEST(displacement_csv_loader_reports_required_header_errors) { FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-MISSING-COLUMN")); } +FESA_TEST(reaction_csv_loader_reports_required_header_errors) { + auto table = fesa::loadReactionCsvFromString("Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2\n" + "1,0,0,0,0,0\n", + "missing-reaction-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" @@ -1024,6 +1049,14 @@ FESA_TEST(displacement_csv_loader_reports_duplicate_node_rows) { FESA_CHECK(fesa::containsDiagnostic(table.diagnostics, "FESA-CSV-DUPLICATE-NODE")); } +FESA_TEST(reaction_csv_loader_reports_duplicate_node_rows) { + auto table = fesa::loadReactionCsvFromString("Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2,RM-RM3\n" + "1,0,0,0,0,0,0\n" + "1,0,0,0,0,0,0\n", + "duplicate-reaction-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" @@ -1050,6 +1083,22 @@ FESA_TEST(displacement_comparator_matches_by_node_id_not_row_order) { FESA_CHECK(compared.pass); } +FESA_TEST(reaction_comparator_matches_by_node_id_not_row_order) { + fesa::FieldOutput actual; + actual.name = "RF"; + actual.position = "NODAL"; + actual.entity_type = "node"; + actual.basis = "GLOBAL"; + actual.entity_ids = {2, 1}; + actual.component_labels = fesa::reactionComponentLabels(); + actual.values = {{{0, 0, 2, 0, 20, 0}}, {{0, 0, 1, 0, 10, 0}}}; + fesa::CsvReactionTable expected; + expected.rows[1] = {1, {0, 0, 1, 0, 10, 0}}; + expected.rows[2] = {2, {0, 0, 2, 0, 20, 0}}; + auto compared = fesa::compareReactions(actual, expected, {1.0e-12, 1.0e-12, 1.0}); + FESA_CHECK(compared.pass); +} + FESA_TEST(displacement_comparator_uses_absolute_and_relative_tolerances) { fesa::FieldOutput actual; actual.name = "U"; @@ -1089,6 +1138,24 @@ FESA_TEST(displacement_comparator_rejects_wrong_component_labels_and_missing_nod FESA_CHECK(fesa::containsDiagnostic(compared.diagnostics, "FESA-COMPARE-MISSING-ACTUAL")); } +FESA_TEST(reaction_comparator_rejects_wrong_component_labels_and_missing_nodes) { + fesa::FieldOutput actual; + actual.name = "RF"; + actual.position = "NODAL"; + actual.entity_type = "node"; + actual.basis = "GLOBAL"; + actual.entity_ids = {1}; + actual.component_labels = fesa::displacementComponentLabels(); + actual.values = {{{0, 0, 0, 0, 0, 0}}}; + fesa::CsvReactionTable expected; + expected.rows[2] = {2, {0, 0, 0, 0, 0, 0}}; + + auto compared = fesa::compareReactions(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"; @@ -1122,9 +1189,13 @@ FESA_TEST(quad02_reference_fixture_discovery_is_consistent) { const auto reference = fesa::loadDisplacementCsv(sourceRoot() + "/references/quad_02_displacements.csv"); FESA_CHECK(!fesa::hasError(reference.diagnostics)); FESA_CHECK(reference.rows.size() == normalized.domain.nodes.size()); + const auto reactions = fesa::loadReactionCsv(sourceRoot() + "/references/quad_02_reactionforces.csv"); + FESA_CHECK(!fesa::hasError(reactions.diagnostics)); + FESA_CHECK(reactions.rows.size() == normalized.domain.nodes.size()); for (const auto& [node_id, node] : normalized.domain.nodes) { (void)node; FESA_CHECK(reference.rows.count(node_id) == 1); + FESA_CHECK(reactions.rows.count(node_id) == 1); } } @@ -1145,6 +1216,24 @@ FESA_TEST(quad02_phase1_stored_displacement_reference_regression) { FESA_CHECK(comparison.max_rel_error <= 1.0e-5); } +FESA_TEST(quad02_phase1_stored_reaction_reference_comparison_reports_current_gap) { + const auto input_text = readTextFile(sourceRoot() + "/references/quad_02_phase1.inp"); + const auto analysis = fesa::runLinearStaticInputString(input_text, "quad_02_phase1.inp"); + FESA_CHECK(analysis.ok()); + FESA_CHECK(analysis.state.converged); + FESA_CHECK(analysis.result_file.steps.size() == 1); + const auto& frame = analysis.result_file.steps[0].frames[0]; + FESA_CHECK(frame.field_outputs.count("RF") == 1); + + const auto expected = fesa::loadReactionCsv(sourceRoot() + "/references/quad_02_reactionforces.csv"); + FESA_CHECK(!fesa::hasError(expected.diagnostics)); + const auto comparison = fesa::compareReactions(frame.field_outputs.at("RF"), expected, {1.0e-6, 1.0e-5, 1.0}); + FESA_CHECK(!comparison.pass); + FESA_CHECK(fesa::containsDiagnostic(comparison.diagnostics, "FESA-COMPARE-TOLERANCE")); + FESA_CHECK(comparison.max_abs_error > 100.0); + FESA_CHECK(comparison.max_rel_error > 0.1); +} + FESA_TEST(mitc4_shape_functions_node_order_and_tying_points) { auto center = fesa::shapeFunctions(0.0, 0.0); const fesa::Real sum = center.n[0] + center.n[1] + center.n[2] + center.n[3]; diff --git a/tests/test_results_module_includes.cpp b/tests/test_results_module_includes.cpp index af877c9..e39ea8b 100644 --- a/tests/test_results_module_includes.cpp +++ b/tests/test_results_module_includes.cpp @@ -91,6 +91,11 @@ int main() { check(required_columns.front() == "Node Label", "CSV node label column changed"); check(required_columns.back() == "UR-UR3", "CSV rotation column changed"); + const auto required_reaction_columns = fesa::reactionCsvRequiredColumns(); + check(required_reaction_columns.size() == 7, "required reaction CSV column count changed"); + check(required_reaction_columns.front() == "Node Label", "reaction CSV node label column changed"); + check(required_reaction_columns.back() == "RM-RM3", "reaction CSV moment 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"); @@ -112,9 +117,21 @@ int main() { 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"); + fesa::CsvReactionTable expected_reactions; + expected_reactions.rows[1] = {1, {0, 0, 3.0, 0, 0, 0}}; + expected_reactions.rows[2] = {2, {0, 0, 0, 0, 0, 0}}; + expected_reactions.rows[3] = {3, {0, 0, 0, 0, 0, 0}}; + expected_reactions.rows[4] = {4, {0, 0, 0, 0, 0, 0}}; + const auto reaction_comparison = fesa::compareReactions(rf_field, expected_reactions, {1.0e-12, 1.0e-12, 1.0}); + check(reaction_comparison.pass, "reaction 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"); + const auto quad02_reactions = fesa::loadReactionCsv(sourceRoot() + "/references/quad_02_reactionforces.csv"); + check(!fesa::hasError(quad02_reactions.diagnostics), "quad_02 reaction CSV no longer loads"); + check(quad02_reactions.rows.size() == 121, "quad_02 reaction CSV row count changed"); + return 0; }