# Step 11: equilibrium-projection-and-reference-gate
## 목적
USER-DECISION-004 option 1과 `AUDIT-001`을 구현·검증한다. 같은 accepted factor-one state에서
accepted free residual, constrained reaction resultant, global applied-plus-reaction balance error를
서로 다른 quantity로 재구성하고, 기존 schema-v1 `/global/equilibrium`에는 balance error만 저장한다.
Fine nonlinear case는 primary, coarse nonlinear case는 companion으로 검증한다. Local nodal
`RF1/RF2/RF3`와 physical spatial `RM1/RM2`는 deterministic review-only evidence이고 blocking
verdict를 바꾸지 않는다. Global force와 moment balance는 각각 fixed normalized `<=1e-8`
comparator gate를 사용하며 runtime configuration 또는 product solver acceptance로 노출하지 않는다.
## 읽어야 할 파일
- `AGENTS.md`
- `docs/PRD.md`
- `docs/ARCHITECTURE.md`
- `docs/ADR.md`
- `docs/CODINGSTYLE.md`
- `docs/TOLERANCE.md`
- `docs/nonlinear-static-newton-raphson/requirements.md`
- `docs/nonlinear-static-newton-raphson/research.md`
- `docs/nonlinear-static-newton-raphson/formulation.md` §§11.4, 13.1, 13.4, 14.1
- `docs/nonlinear-static-newton-raphson/numerical-review.md` §§3.2, 6.2, 6.3
- `docs/nonlinear-static-newton-raphson/reference-model.md` §§3–6
- `docs/nonlinear-static-newton-raphson/io.md` §§7.3–7.5.1, 8.1–8.7
- `docs/nonlinear-static-newton-raphson/implementation-plan.md`
- `docs/nonlinear-static-newton-raphson/corrections.md` §13
- `phases/nonlinear-static-newton-raphson/index.json`
- `include/fesa/analysis/nonlinear_analysis_state.h`
- `include/fesa/analysis/nonlinear_static_procedure.h`
- `include/fesa/analysis/nonlinear_static_control.h`
- `src/fesa/analysis/nonlinear_static_procedure.cpp`
- `src/fesa/analysis/nonlinear_static_control.cpp`
- `include/fesa/results/nonlinear_results_writer.h`
- `include/fesa/io/hdf5/nonlinear_hdf5_results_writer.h`
- `src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp`
- `tests/integration/analysis/nonlinear_static_procedure_test.cpp`
- `tests/unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp`
- `tests/unit/analysis/nonlinear_static_control_test.cpp`
- `tests/unit/analysis/newton_iteration_log_test.cpp`
- `tests/reference/reference_tolerance_policy.h`
- `tests/reference/reference_tolerance_policy.cpp`
- `tests/reference/nonlinear_static_reference_comparison.h`
- `tests/reference/nonlinear_static_reference_comparison.cpp`
- `tests/reference/nonlinear_static_reference_cases_test.cpp`
- `tests/CMakeLists.txt`
- `.harness/config.json`
- `CMakePresets.json`
## 이전 Step과 보존 전제
- Steps 0–10은 completed다. 그 status, summary, timestamp, commit 또는 구현을 replay, amend,
reset하거나 다시 작성하지 않는다.
- 현재 worktree의 `NSNR-CTRL-018..021`, `NSNR-LOG-012`, `NSNR-PROC-019`,
`NSNR-HDF5-014`, residual-selector GREEN과 `NSNR-REF-001..023` comparator/CMake 작업은
이전 blocked Step 11에서 생성된 보존 대상이다. 삭제·재생성·reset하지 말고 현재 내용에서
계속한다.
- 현재 comparator의 shared `<=1e-10` global force/moment gate는 이전 blocker를 재현하는
보존된 implementation state다. 새 RED가 approved between-threshold metric을 거부하는 것을
증명한 뒤 comparator 내부 gate만 `<=1e-8`로 변경한다.
- Selector RED는 이미 과거 실행 evidence에 속한다. 이 재실행에서 selector RED를 fabricate하거나
다시 실패시키지 않는다. 기존 테스트가 계속 green인지 focused VERIFY에서 확인한다.
- Step 12는 pending이며 이 Step에서 시작하지 않는다.
## 소유 파일
RED test를 먼저 추가한 뒤 필요한 production file만 최소 수정한다.
- Modify test first: `tests/integration/analysis/nonlinear_static_procedure_test.cpp`
- Modify test first: `tests/unit/io/hdf5/nonlinear_hdf5_results_writer_test.cpp`
- Preserve and extend test-private comparator: `tests/reference/nonlinear_static_reference_comparison.h`
- Preserve and extend test-private comparator: `tests/reference/nonlinear_static_reference_comparison.cpp`
- Preserve and extend tests: `tests/reference/nonlinear_static_reference_cases_test.cpp`
- Preserve existing registration; edit only if the current sources are not registered:
`tests/CMakeLists.txt`
- Minimal GREEN: `src/fesa/analysis/nonlinear_static_procedure.cpp`
- Minimal GREEN: `src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp`
- Preserve existing partial GREEN/tests without redesign:
`src/fesa/analysis/nonlinear_static_control.cpp`,
`tests/unit/analysis/nonlinear_static_control_test.cpp`,
`tests/unit/analysis/newton_iteration_log_test.cpp`
No public header, new CMake target, new HDF5 dataset, schema-version change or reference artifact is
owned by this Step.
## Authoritative HDF5 입력과 test-private report 경계
Comparator는 아래 existing schema-v1 paths만 읽는다.
```text
/model/nodes [node_count]
/model/nodal_constraint_mask [node_count,6]
/steps/Step-1/accepted_frames unique factor-one row
/steps/Step-1/frames//nodal/displacement [node_count,6]
/steps/Step-1/frames//nodal/generalized_internal_force [node_count,6]
/steps/Step-1/frames//nodal/generalized_external_force [node_count,6]
/steps/Step-1/frames//nodal/generalized_residual [node_count,6]
/steps/Step-1/frames//nodal/reaction [node_count,6]
/steps/Step-1/frames//global/equilibrium [6]
```
`/global/equilibrium` component order is
`FORCE_1,FORCE_2,FORCE_3,MOMENT_1,MOMENT_2,MOMENT_3` and its required
`location` is `global-balance-error`.
Preserve the current test-private types and make only the extensions needed by the approved
dispositions and distinct internal evidence. Add the required standard `` and ``
includes to the test-private header. Candidate signature-level shape:
```cpp
namespace fesa::test {
enum class NonlinearReferenceDisposition {
kBlocking,
kWarningOnly,
kReviewOnly,
kExcluded,
};
struct NonlinearReferencePrecheckFinding {
std::string family_identity;
NonlinearReferenceDisposition disposition;
std::string diagnostic_code;
std::vector affected_keys;
};
struct NonlinearInternalEquilibriumReport {
std::string family_identity;
std::size_t row_count;
double numerator;
double scale;
std::optional normalized_metric;
bool passed;
std::string diagnostic_code;
};
struct ConstrainedReactionResultantReport {
std::array force;
std::array moment;
};
struct NonlinearReferenceComparisonReport {
std::string case_id;
std::filesystem::path original_input_path;
std::filesystem::path copied_input_path;
std::filesystem::path generated_results_hdf5;
std::filesystem::path generated_log_path;
std::vector pre_run_artifacts;
std::vector post_run_artifacts;
std::vector families;
std::vector precheck_findings;
std::vector internal_equilibrium;
ConstrainedReactionResultantReport constrained_reaction_resultant;
bool external_blocking_passed;
bool internal_blocking_passed;
bool blocking_passed;
};
} // namespace fesa::test
```
These are test-support interfaces only. Match the existing aggregate/data style and avoid a public
solver API. For an exact-zero internal scale, leave `normalized_metric` empty and use
`diagnostic_code` plus the exact-zero numerator/pass rule instead of writing NaN or Inf.
## Required physical algorithm
For each node in stable Domain order at the accepted state:
1. Read `X` from Domain/model identity and `u, phi` from the accepted state. Set `x=X+u`.
2. Read generalized applied force/covector from `generalized_external_force`.
3. Build constrained generalized reaction by applying the exact component mask to
`generalized_residual` before any rotational conversion.
4. Convert applied and constrained-reaction rotational covectors independently with
`InverseTransposeLeftJacobianSO3(phi)`. Do not convert their sum as a shortcut and do not mix
free generalized residual into reaction.
5. Accumulate, about global origin `[0,0,0]`:
```text
F_applied += applied spatial force
M_applied += x cross F_applied_node + applied spatial physical moment
F_reaction += constrained reaction spatial force
M_reaction += x cross F_reaction_node + reaction spatial physical moment
global balance error = [F_applied + F_reaction,
M_applied + M_reaction]
```
Store only the six-component balance error in the existing accepted-frame
`global_equilibrium` and existing `/global/equilibrium` dataset. Reconstruct the constrained
reaction resultant and accepted free residual separately in the comparator; do not add summary
datasets or a public header.
For internal blocking gates:
- Force scale is `max(sum(||F_applied_node||2), sum(||F_reaction_node||2))`.
- Moment scale is
`max(sum(||x cross F_applied_node + M_applied_node||2),
sum(||x cross F_reaction_node + M_reaction_node||2))`.
- Force and moment use separate `||balance_error||2/scale <= 1e-8` gates. If scale is exact zero,
only exact-zero balance passes. No floor, `max(1,scale)` or clamp is allowed.
- Accepted free translation-force and rotation-moment rows use the exact free mask and reproduce
NSNR-069: nonzero matching external scale uses
`||residual||inf/max(||internal||inf,||external||inf) <= 1e-2`; exact-zero external scale uses
typed absolute `<=1e-3`. Finite checks occur first.
- `docs/TOLERANCE.md` is not used for these internal gates.
## RED
기존 dirty `NSNR-PROC-019`, `NSNR-HDF5-014`, `NSNR-REF-020..023`은 재작성하지 않는다.
먼저 existing `NSNR-REF-022`를 실행해 old shared gate failure를 다시 관찰·기록한 뒤,
`NSNR-REF-024..025`를 comparator threshold 변경 전에 test-first로 추가한다. Procedure/HDF5
behavior는 threshold rework를 위해 추가 변경하지 않는다.
- Approved draft label `NSNR-PROC-011` collides with completed Step 10's historical
`NSNR-PROC-011` increment-loop identity. Preserve that history and use collision-resolved test ID
`NSNR-PROC-019` with exact GTest symbol
`NonlinearStaticProcedure.NonzeroPrescribedMotionWritesCurrentArmSpatialAppliedPlusReactionBalance`.
Use a supported nonlinear shell input with nonzero prescribed translation and rotation plus a
resolved fixed-global load. Independently reconstruct the six balance components from accepted
`X,u,phi`, external force and masked residual. Expected RED: the current helper uses initial
coordinates, raw full-residual summation and raw rotational residual, so the current-arm/spatial
expected vector or distinct constrained/free projection assertion fails.
- `NSNR-HDF5-014`
`NonlinearHdf5ResultsWriter.WritesSchemaV1GlobalBalanceErrorLocation` checks existing path/shape,
exact component names, schema version 1 and `location=global-balance-error`. Expected RED: current
writer emits `location=global-origin`.
- `NSNR-REF-020`
`NonlinearStaticReference.AuditsFinePrimaryAndCoarseCompanionArtifacts` checks all exact fine,
coarse and linear input/CSV hashes, fine/coarse independent execution directories and case roles.
Expected RED: current case inventory supports only coarse nonlinear and linear cases.
- `NSNR-REF-021`
`NonlinearStaticReference.ReviewOnlyLocalReactionsCannotChangeBlockingVerdict` injects or observes
failing local RF/RM row/RMS decisions while U and internal blocking families pass. It requires
deterministic local error/precheck reporting and an unchanged blocking pass. Expected RED:
current local RF/RM dispositions are `kBlocking`.
- `NSNR-REF-022`
`NonlinearStaticReference.BlocksOnGlobalBalanceAndAcceptedFreeResidual` fails each coarse/fine
global-force, global-moment, free-force-residual and free-moment-residual family one at a time,
then passes all. It also asserts the constrained reaction resultant is a distinct review summary.
Expected RED for this re-execution: the preserved comparator applies the old shared `<=1e-10`
gate, so the approved coarse metric `1.413750912531167e-10` and fine metric
`2.7454660159924086e-10` are rejected even though both are below `1e-8`.
- `NSNR-REF-023`
`NonlinearStaticReference.RejectsMissingExtraDuplicateAndNonfiniteRowsBeforeToleranceAndRecordsReviewOnlyFindings`
covers blocking, warning-only and review-only families. Blocking precheck failure changes the
blocking verdict; warning/review failures remain ordered findings. Expected RED: the current
comparator cannot retain review-only precheck findings independently of the blocking verdict.
- `NSNR-REF-024`
`NonlinearStaticReference.GlobalForceAndMomentBalanceUseApprovedInclusiveOneEighthGate` uses a
test-private balance evaluator for distinct force and moment family identities. It requires an
approved between-threshold metric such as `5e-9` and exact `1e-8` to pass, and
`nextafter(1e-8,+infinity)` to fail for both identities. Expected decisive RED: the preserved
shared `<=1e-10` comparison rejects `5e-9` and `1e-8`.
- `NSNR-REF-025`
`NonlinearStaticReference.GlobalForceAndMomentExactZeroScaleRequiresExactZeroBalance` exercises
force and moment identities independently and requires exact-zero scale plus exact-zero numerator
to pass while any nonzero numerator fails without NaN/Inf or a fabricated normalized metric. This
is an unchanged-contract regression and may already pass in the RED batch; it must be present
before GREEN.
Run and record the observed failures:
```powershell
cmake --build --preset windows-msvc-ninja-debug
ctest --preset windows-msvc-ninja-debug -R "^(NonlinearStaticProcedure|NonlinearHdf5ResultsWriter|NonlinearStaticReference)\." --output-on-failure
```
The new tests must fail for the stated reasons. A compile failure caused only by the test-first
enum/report extension is valid RED when the diagnostic identifies the missing planned production/
test-support behavior. An unexpected pass or unrelated failure stops the Step for classification.
## Minimal GREEN
1. Replace the private raw-residual/initial-coordinate global helper in
`src/fesa/analysis/nonlinear_static_procedure.cpp` with the Required physical algorithm above.
Keep stable node order, mask-before-conversion, independent applied/reaction conversion and
finite failure behavior. Do not change the public procedure header or accepted-state layout.
2. In `src/fesa/io/hdf5/nonlinear_hdf5_results_writer.cpp`, preserve the path, shape, component
names and schema version; change the exact `/global/equilibrium` location attribute to
`global-balance-error` and keep writer validation finite/fail-closed.
3. Extend the existing comparator without deleting its partial work:
- add fine primary inventory and keep coarse companion/linear control independent;
- add `kReviewOnly` and keep local RF/RM key/schema/inventory/finite/row/RMS evidence;
- reconstruct the constrained reaction resultant and accepted free residual from the exact HDF5
paths/mask; never use `/nodal/reaction` or `/global/equilibrium` as a substitute for all three;
- apply internal gates exactly as specified above, using one test-private/internal shared
`1.0e-8` constant for both distinct force and moment families;
- keep U blocking, UR warning-only, local RF/RM review-only, RM3/stress excluded;
- reject blocking missing/extra/duplicate/nonfinite rows before tolerance and retain ordered
nonblocking findings;
- set overall blocking verdict from blocking external U families AND all internal global/free
families only.
4. Preserve existing `NSNR-CTRL-018..021`, `NSNR-LOG-012`, Full Newton, fixed factors, iteration
limit, 39-field log schema and the already corrected residual selector unchanged.
5. Do not change production solver/configuration behavior for the balance threshold. No `fesa.env`,
environment variable, CLI option, public API or HDF5 schema change is part of this GREEN.
## Exact read-only artifact inventory
| case | exact path | rows | SHA-256 |
| --- | --- | ---: | --- |
| fine primary | `reference/shellNL-meshfine/shellNL-meshfine.inp` | input | `40DF1CE77311C8D799564A21A60F3BDEBB8148D52CAE22B7292FDFAD32018220` |
| fine primary | `reference/shellNL-meshfine/shellNL-meshfine displacements.csv` | 729 | `740819FF8AED0E279961581F7CDBFA12784CFBA3A488C35A652E80103F64433F` |
| fine primary | `reference/shellNL-meshfine/shellNL-meshfine reactions.csv` | 729 | `AEB01C98C596792B0E5F2B86A81945CAB0BEB0C6969CFC6738C6F81DF1F94363` |
| fine optional | `reference/shellNL-meshfine/shellNL-meshfine stresses.csv` | 5,408 | `6D6213AB5DB9D6B1F5FBD641367D08BACC805967B6C5A95BCE0E1A5395377782` |
| coarse companion | `reference/shellNL/shellNL.inp` | input | `4BF3ECB0F7F1E8462C573E993DBC97AC5D920C91CF6F23AD4C6891D77FFBADA2` |
| coarse companion | `reference/shellNL/shellNL displacements.csv` | 49 | `4E0F99CA9372B1ADF7FAF102F43BC0B887DC743FDEDA378E4342D2CF8E0C0786` |
| coarse companion | `reference/shellNL/shellNL reactions.csv` | 49 | `001CB1541A9E94424E5E650509D46BC8724970DF4DE47184D9DB09C7938AB069` |
| coarse optional | `reference/shellNL/shellNL stresses.csv` | 288 | `5500E5114B1C44887528E30CEC054A1EB0F68BE3F4B8CE81FA59FC74A881483F` |
| linear control | `reference/shell/shell.inp` | input | `4005851E1AB22FD3A16AC17A8D5DA3E051233F69F37419079F3553AD134ECFCF` |
| linear control | `reference/shell/shell displacements.csv` | 49 | `C81D94E0B4A849F87AA0F79C83A79B94D5661AC79E44ED826919AB432C87746B` |
| linear optional | `reference/shell/shell reactions.csv` | 49 | `0E34EEC27E700C3C0C744C1DAFD8A3B95EFEF34B8616FE96665876586FE82D41` |
| linear optional | `reference/shell/shell stresses.csv` | 288 | `2B75A5B0DEB045A283C9BB7E9B1153B79F7537FF487214BF4A23316B59B660A7` |
For every case, snapshot original inventory, bytes, SHA-256, size and timestamp; copy only the `.inp`
bytes to a unique build-local directory; run only the copy; consume copied-sibling `_results.h5` and
`_log.txt`; compare against original read-only CSVs; then prove the original inventory is unchanged.
## External comparison and classification
- Fine and coarse `U1/U2/U3` are separate blocking families.
- Linear `U1/U2/U3` is a blocking regression family.
- All `UR1/UR2/UR3` families are warning-only.
- Fine/coarse local `RF1/RF2`, `RF3`, spatial `RM1/RM2` are separate review-only families with exact
mask-derived keys and complete common-policy error reports.
- `RM3` and all stress values have no numerical family. Stress files remain hash-audited optional
inventory; the known fine malformed optional stress key is not repaired.
- External families use exactly `docs/TOLERANCE.md`: reference-only `S=max(abs(reference))`,
`B=0.01*S`, near-zero row `abs(error)<=B`, other-row relative `<=0.05`, family
`RMS(error)/S<=0.01`, exact-zero family exact-zero FESA only. No absolute floor, averaging,
collapse, row-order identity, clamp or tolerance relaxation.
The final gate order is literally:
```text
ARTIFACT CHECK -> COMPARE -> CLASSIFY -> REPORT
```
## Focused and full VERIFY
After minimal GREEN, run in this order and retain command, exit code, duration and output tail for
Step 12:
```powershell
cmake --preset windows-msvc-ninja-debug
cmake --build --preset windows-msvc-ninja-debug
ctest --preset windows-msvc-ninja-debug -R "^(NonlinearConvergence|NewtonIterationLog)\." --output-on-failure
ctest --preset windows-msvc-ninja-debug -R "^(NonlinearStaticProcedure|NonlinearHdf5ResultsWriter)\." --output-on-failure
ctest --preset windows-msvc-ninja-debug -R "^NonlinearStaticReference\." --output-on-failure
ctest --preset windows-msvc-ninja-debug -R "^(NonlinearStaticReference|ReferenceTolerance|Mitc4Reference)\." --output-on-failure
ctest --preset windows-msvc-ninja-debug --show-only=json-v1
ctest --preset windows-msvc-ninja-debug --output-on-failure
```
The Harness launch environment must be VsDevCmd x64 and inherit `FESA_GTEST_SOURCE_DIR`, `MKL_DIR`,
`TBB_DIR`, and `HDF5_DIR`. Do not substitute the solution-only MSBuild path.
## Acceptance Criteria
- New RED tests fail first for their declared current behavior, then pass after minimal GREEN.
- `/global/equilibrium` remains schema v1 `[6]` with the same component names and stores only the
current-arm/spatial-moment applied-plus-reaction balance error with
`location=global-balance-error`.
- Fine/coarse global force and moment normalized metrics are each `<=1e-8`; exact-zero scale follows
the exact-zero rule.
- Fine/coarse accepted free force and moment residual gates pass NSNR-069 independently.
- Fine/coarse U blocking and linear U regression pass `docs/TOLERANCE.md`.
- UR differences remain warning-only. Local RF/RM findings remain review-only and cannot fail the
blocking verdict. RM3/stress remain excluded.
- Missing/extra/duplicate/nonfinite blocking rows fail before tolerance; warning/review findings are
deterministic and nonblocking.
- Original reference artifact content, inventory, size, timestamp and hash are unchanged.
- Focused tests, test discovery and full CTest pass without new compiler warnings.
## 상태 기록
On success, update only Step 11 to `completed` and add one concise `summary`. On an implementation,
build, test or reference-comparison failure, classify it and update only the Executor-selected Step's
allowed payload. Executor owns retry, timestamps, commits, advancement and top-level status. Do not
start Step 12.
## 금지사항
- Do not edit any reference artifact. 이유: exact stored files are immutable external evidence.
- Do not run FESA on an original reference `.inp`. 이유: derived sibling outputs would mutate the
reference directory.
- Do not add HDF5 datasets, change schema version/path/shape/components or add public headers. 이유:
Stage 5 requires reconstruction from existing schema-v1 datasets.
- Do not use initial coordinates or raw rotational covectors for global physical moment. 이유:
`AUDIT-001` requires accepted current arms and spatial moment conversion.
- Do not apply the constraint mask after spatial conversion. 이유: partial rotational constraints
would mix free residual into reaction.
- Do not use `/global/equilibrium`, local `/nodal/reaction` or a raw full-residual sum as a substitute
for all three equilibrium quantities. 이유: their physical identities differ.
- Do not make local reaction rows blocking or omit their review evidence. 이유: USER-DECISION-004
fixes review-only with deterministic findings.
- Do not compare `RM3` or stress values, average/collapse rows, match by row order, clamp zero values,
add `max(1,S)`, relax tolerance or repair CSVs. 이유: approved identity/tolerance contracts forbid it.
- Do not add `fesa.env`, runtime configuration, product solver acceptance, CLI or public API behavior
for the `1e-8` gate. 이유: this fixed threshold belongs only to comparator-owned internal verification.
- Do not replay or fabricate selector RED, delete partial Step 11 work, reset history, commit, push,
run a reference solver, start Step 12, or claim Physics/Release readiness.
- Stop and return a classified blocker if the required behavior needs an upstream contract, public
API, HDF5 schema, tolerance or reference-artifact change.