test: onboard quad02 reaction reference

This commit is contained in:
NINI
2026-05-05 23:56:27 +09:00
parent 9741671f70
commit c47557885d
15 changed files with 597 additions and 24 deletions
+1
View File
@@ -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를 함께 사용할 것
## 명령어
+6 -5
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.
## 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?
+41 -3
View File
@@ -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.
+1
View File
@@ -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.
+3 -1
View File
@@ -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:
+21
View File
@@ -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:
+3 -1
View File
@@ -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.
+32 -2
View File
@@ -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/<case_name>_reactionforces.csv
references/<case_name>_reactions.csv
```
`*_reactionforces.csv` and `*_reactions.csv` map to:
```text
/results/steps/<step>/frames/<frame>/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.
+46 -5
View File
@@ -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
<case_name>_reactionforces.csv
<case_name>_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/<step>/frames/<frame>/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.
@@ -26,10 +26,24 @@ struct CsvDisplacementTable {
std::vector<Diagnostic> diagnostics;
};
struct CsvReactionRow {
GlobalId node_id = 0;
std::array<Real, 6> values{};
};
struct CsvReactionTable {
std::map<GlobalId, CsvReactionRow> rows;
std::vector<Diagnostic> diagnostics;
};
inline std::vector<std::string> displacementCsvRequiredColumns() {
return {"Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
}
inline std::vector<std::string> 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<std::string> required = reactionCsvRequiredColumns();
std::vector<std::string> headers = splitCsv(line);
std::map<std::string, std::size_t> column;
for (std::size_t i = 0; i < headers.size(); ++i) {
column[trim(headers[i])] = i;
}
for (const std::string& name : required) {
if (column.count(name) == 0) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-MISSING-COLUMN", "Missing CSV column: " + name, {source_name, 1, ""}});
}
}
if (hasError(table.diagnostics)) {
return table;
}
LocalIndex line_number = 1;
while (std::getline(input, line)) {
++line_number;
if (trim(line).empty()) {
continue;
}
std::vector<std::string> fields = splitCsv(line);
auto get = [&](const std::string& name) -> std::string {
const std::size_t index = column[name];
return index < fields.size() ? fields[index] : "";
};
auto node_id = parseInt64(get("Node Label"));
if (!node_id) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-NODE", "Invalid node label", {source_name, line_number, ""}});
continue;
}
if (table.rows.count(*node_id) != 0) {
table.diagnostics.push_back({Severity::Error, "FESA-CSV-DUPLICATE-NODE", "Duplicate node label", {source_name, line_number, ""}});
continue;
}
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 = "<memory>") {
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<GlobalId, std::array<Real, 6>> actual_by_node;
const std::size_t actual_count = std::min(actual.entity_ids.size(), actual.values.size());
for (std::size_t i = 0; i < actual_count; ++i) {
if (actual_by_node.count(actual.entity_ids[i]) != 0) {
result.diagnostics.push_back(
{Severity::Error, "FESA-COMPARE-DUPLICATE-ACTUAL", "FESA 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<Real>::infinity());
result.max_abs_error = std::max(result.max_abs_error, abs_error);
result.max_rel_error = std::max(result.max_rel_error, rel_error);
if (!(abs_error <= options.abs_tol || rel_error <= options.rel_tol)) {
const std::string component_label = 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
+24 -2
View File
@@ -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.
+38 -1
View File
@@ -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.
+122
View File
@@ -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
1 Node Label RF-RF1 RF-RF2 RF-RF3 RM-RM1 RM-RM2 RM-RM3
2 1 -8.07E-13 -5.43E-14 6.86E+03 9.09E-13 2.40E+04 8.36E-15
3 2 0 0 0 0 0 0
4 3 -2.21E-13 6.87E-13 6.86E+03 2.40E+04 -1.82E-12 2.53E-16
5 4 -4.31E-14 8.33E-14 -743.226746 490.872711 490.872711 0
6 5 2.48E-13 -5.84E-14 6.86E+03 4.09E-12 -2.40E+04 4.70E-15
7 6 -1.84E-13 -2.04E-13 -743.226746 490.872711 -490.872711 0
8 7 -6.16E-13 -3.42E-13 6.86E+03 -2.40E+04 9.09E-13 -4.89E-15
9 8 1.02E-13 -1.15E-13 -743.226746 -490.872711 -490.872711 0
10 9 -6.54E-14 -1.07E-13 -743.226746 -490.872711 490.872711 0
11 10 0 0 0 0 0 0
12 11 0 0 0 0 0 0
13 12 0 0 0 0 0 0
14 13 0 0 0 0 0 0
15 14 0 0 0 0 0 0
16 15 0 0 0 0 0 0
17 16 0 0 0 0 0 0
18 17 0 0 0 0 0 0
19 18 -1.36E-13 -1.05E-13 5.97E+03 2.17E+04 819.817993 -7.10E-15
20 19 -3.44E-13 8.09E-13 3.67E+03 1.58E+04 1.42E+03 -1.32E-15
21 20 2.68E-13 -1.41E-12 749.094788 8.27E+03 1.42E+03 -2.01E-15
22 21 2.55E-13 -2.02E-13 -947.392273 2.61E+03 822.967346 -5.79E-16
23 22 5.13E-13 -3.77E-13 -947.392273 822.967346 2.61E+03 1.42E-15
24 23 -5.10E-13 -2.58E-13 749.094788 1.42E+03 8.27E+03 6.24E-15
25 24 -1.15E-13 -2.30E-13 3.67E+03 1.42E+03 1.58E+04 7.89E-15
26 25 4.34E-13 5.17E-14 5.97E+03 819.817993 2.17E+04 9.21E-15
27 26 0 0 0 0 0 0
28 27 0 0 0 0 0 0
29 28 0 0 0 0 0 0
30 29 0 0 0 0 0 0
31 30 -1.14E-13 -1.53E-13 5.97E+03 819.817993 -2.17E+04 -8.24E-15
32 31 -1.27E-12 3.44E-13 3.67E+03 1.42E+03 -1.58E+04 -1.06E-14
33 32 -1.15E-13 -5.05E-13 749.094788 1.42E+03 -8.27E+03 -1.60E-14
34 33 -1.12E-13 -3.07E-14 -947.392273 822.967346 -2.61E+03 -1.26E-14
35 34 -4.03E-14 -1.92E-13 -947.392273 2.61E+03 -822.967346 1.18E-14
36 35 1.19E-13 2.40E-13 749.094788 8.27E+03 -1.42E+03 1.14E-14
37 36 -6.27E-14 8.10E-13 3.67E+03 1.58E+04 -1.42E+03 6.78E-15
38 37 6.27E-14 1.81E-13 5.97E+03 2.17E+04 -819.817993 1.68E-15
39 38 0 0 0 0 0 0
40 39 0 0 0 0 0 0
41 40 0 0 0 0 0 0
42 41 0 0 0 0 0 0
43 42 -5.00E-14 1.36E-12 5.97E+03 -2.17E+04 -819.817993 -5.44E-15
44 43 9.99E-14 2.80E-13 3.67E+03 -1.58E+04 -1.42E+03 -9.03E-15
45 44 -2.16E-13 8.84E-14 749.094788 -8.27E+03 -1.42E+03 -8.14E-15
46 45 -3.92E-13 -7.78E-14 -947.392273 -2.61E+03 -822.967346 -6.83E-15
47 46 1.93E-13 2.02E-13 -947.392273 -822.967346 -2.61E+03 -3.86E-15
48 47 -9.65E-13 -1.51E-14 749.094788 -1.42E+03 -8.27E+03 3.14E-15
49 48 9.48E-13 8.02E-14 3.67E+03 -1.42E+03 -1.58E+04 5.00E-15
50 49 -7.88E-13 5.70E-14 5.97E+03 -819.817993 -2.17E+04 5.91E-15
51 50 1.65E-13 -4.66E-14 5.97E+03 -819.817993 2.17E+04 1.72E-15
52 51 1.08E-13 1.59E-13 3.67E+03 -1.42E+03 1.58E+04 -5.18E-15
53 52 7.29E-13 1.81E-13 749.094788 -1.42E+03 8.27E+03 -3.90E-15
54 53 -2.53E-13 1.69E-14 -947.392273 -822.967346 2.61E+03 8.11E-15
55 54 5.14E-14 -4.00E-14 -947.392273 -2.61E+03 822.967346 -3.97E-15
56 55 -6.50E-14 1.41E-13 749.094788 -8.27E+03 1.42E+03 4.21E-15
57 56 3.71E-14 -8.15E-14 3.67E+03 -1.58E+04 1.42E+03 1.99E-15
58 57 4.68E-13 5.31E-13 5.97E+03 -2.17E+04 819.817993 4.83E-15
59 58 0 0 0 0 0 0
60 59 0 0 0 0 0 0
61 60 0 0 0 0 0 0
62 61 0 0 0 0 0 0
63 62 0 0 0 0 0 0
64 63 0 0 0 0 0 0
65 64 0 0 0 0 0 0
66 65 0 0 0 0 0 0
67 66 0 0 0 0 0 0
68 67 0 0 0 0 0 0
69 68 0 0 0 0 0 0
70 69 0 0 0 0 0 0
71 70 0 0 0 0 0 0
72 71 0 0 0 0 0 0
73 72 0 0 0 0 0 0
74 73 0 0 0 0 0 0
75 74 0 0 0 0 0 0
76 75 0 0 0 0 0 0
77 76 0 0 0 0 0 0
78 77 0 0 0 0 0 0
79 78 0 0 0 0 0 0
80 79 0 0 0 0 0 0
81 80 0 0 0 0 0 0
82 81 0 0 0 0 0 0
83 82 0 0 0 0 0 0
84 83 0 0 0 0 0 0
85 84 0 0 0 0 0 0
86 85 0 0 0 0 0 0
87 86 0 0 0 0 0 0
88 87 0 0 0 0 0 0
89 88 0 0 0 0 0 0
90 89 0 0 0 0 0 0
91 90 0 0 0 0 0 0
92 91 0 0 0 0 0 0
93 92 0 0 0 0 0 0
94 93 0 0 0 0 0 0
95 94 0 0 0 0 0 0
96 95 0 0 0 0 0 0
97 96 0 0 0 0 0 0
98 97 0 0 0 0 0 0
99 98 0 0 0 0 0 0
100 99 0 0 0 0 0 0
101 100 0 0 0 0 0 0
102 101 0 0 0 0 0 0
103 102 0 0 0 0 0 0
104 103 0 0 0 0 0 0
105 104 0 0 0 0 0 0
106 105 0 0 0 0 0 0
107 106 0 0 0 0 0 0
108 107 0 0 0 0 0 0
109 108 0 0 0 0 0 0
110 109 0 0 0 0 0 0
111 110 0 0 0 0 0 0
112 111 0 0 0 0 0 0
113 112 0 0 0 0 0 0
114 113 0 0 0 0 0 0
115 114 0 0 0 0 0 0
116 115 0 0 0 0 0 0
117 116 0 0 0 0 0 0
118 117 0 0 0 0 0 0
119 118 0 0 0 0 0 0
120 119 0 0 0 0 0 0
121 120 0 0 0 0 0 0
122 121 0 0 0 0 0 0
+93 -4
View File
@@ -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<std::string>({"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];
+17
View File
@@ -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;
}