diff --git a/docs/abaqus-tolerance/physics-validation.md b/docs/abaqus-tolerance/physics-validation.md new file mode 100644 index 0000000..5b185fa --- /dev/null +++ b/docs/abaqus-tolerance/physics-validation.md @@ -0,0 +1,160 @@ +# T5 후보 물리·시간 정확도 검증 + +검사일: 2026-10-19. pinned parent `dd54b0ae1e68a313ac46933b886c865c85ec4432`, +branch `dryforge/tolerance-t5`. 이 기록은 T5의 선택 검사다. **물리 검증 closure는 +FAIL이며, 후보 production 경로의 비선형 시간 정확도 R8은 미완료다.** 새 강체 병진 +oracle의 통과와 기존 strict algebra 검사의 통과로 전체 정확도·채택 가능성을 주장하지 않는다. + +## 변경·독립 검토 + +변경은 기존 `hht_dynamic_procedure_test.cpp`의 +`HhtDynamicProcedure.CandidateRigidTranslationMatchesIndependentPhysicalOracle` 한 검사와 +이 기록뿐이다. Production·local kernel·T3 helper·probe·기존 판정 기준은 수정하지 않았다. +기존 검사 파일은 `tests/CMakeLists.txt`와 `msvc/fesa_unit_tests.vcxproj` 양쪽에 등록되어 +있어 추가 source 등록은 필요 없다. 전체 CMake 실행은 T7 범위다. + +새 검사는 `HhtDynamicProcedureExecutor::Execute` → `ExecuteReducedHht`의 실제 후보 +경로를 호출한다. Fixture는 기존 두 disconnected unit-square shell의 density=2, +thickness=.1, area=1을 그대로 사용한다. Nodal-lumped translational mass는 각 node +`.05`, 총 질량은 `.4`다. 여덟 node 각각에 일정 global x CLOAD=1이므로 총 힘은8, +초기 및 이후 물리 가속도는20이다. 시간은 `h=.25,T=.5,N=2,INC=3`이며 source의 +최대 증분을 변경하지 않았다. 독립 연속체 강체 해는 다음과 같다. + +```text +q_x(t) = 10 t^2, v_x(t) = 20 t, physical a_x(t) = 20 +public A_x(t) = (1+alpha)20-alpha*20 = 20 +T(t) = .5*.4*(20t)^2 = 80t^2 +W(t) = 8*(10t^2) = 80t^2 +Uphysical = Udrill = energy defect = 0 +``` + +Frame0 및 두 endpoint를 모두 비교한다. All-free와 y/z/rotation을 영구속한 두 경우를 +각각 실행하여 full q/v/a/A의 나머지 성분과 영 BC를 검사한다. 입력 힘, nodal inertia, +전역 force/moment balance, kinetic/physical/drilling energy, 일정 외력 work/defect와 +두 positive increment의 `hht-log-v2`, policy ID, field_pass/accepted를 확인한다. +일반 수치오차 상한은 사용자 승인 실험의 `1e-3`이다. 단위 scale은 L=1,T=.5,E=20이며, +정확한 입력 힘·schedule·영 BC와 필수 global equilibrium `1e-8`은 그대로 검사한다. +이 값은 Abaqus 기본값이나 production 내부 acceptance 완화를 뜻하지 않는다. + +Root의 독립 검토는 source의 두 단위면적·rho·두께·CLOAD에서 M=.4/F=8/a=20을 +재계산하고 q/v/a·energy 식, scale, 양 BC 및 로그 의미를 대조했다. 처음 실행한 더 +엄격한 진단 버전도 통과했으나 최종 검사의 일반 오차 상한은 승인된 `1e-3`이다. +정확한 강체 해는 시간 오차가 roundoff 수준이고 a와 A도 같으므로 **차수 >=1.8, +N7 separation 또는 비상수 physical a/A 구분의 증거가 아니다.** 해당 구분은 아래의 +기존 algebra/rate/writer 검사 범위와 함께 해석해야 한다. + +## 현재 실행 evidence + +공유 evidence root는 workspace root 기준 +`.build/tolerance-experiment/go-20261018/T5/`다. 실제 cwd는 +`.dryforge/worktrees/T2`이며 다른 workspace·main·dev는 수정하지 않았다. +원본 reference·`reference/temp`·외부 Abaqus 실행·다운로드는 사용하지 않았다. + +| 실행 | 실제 선택 | PASS | FAIL | SKIP | native exit | +| --- | ---: | ---: | ---: | ---: | ---: | +| 최종 Native Release probe ON |153|144|8|1|1| +| 최종 Native Debug probe ON |28|27|0|1|0| +| 보존 strict Release C08 대조 |5|0|5|0|1| + +Release/Debug unit discovery는 각각887개로 identity가 일치했다. 최종 filter의 선택 +identity와 XML을 대조하여 missing/extra/duplicate=0을 확인했다. 범위 밖 identity는 +`verification.json`의 `excluded`에 보존했다. 위 행은 서로 중복되는 검사를 포함하므로 +합쳐서 unique 통과 수로 보고하지 않는다. Release153의 elapsed69.325초와 +Debug28의25.638초는 일부 동시 검사의 verification timing이며 공식 성능 측정이 아니다. + +- `final-selected-release.command.txt`, `.raw.txt`, `.xml`, `.exit.txt`: + 물리·time/algebra·reduced/rate/partial-BC·energy·state/writer·요소 정확도153개. +- `final-selected-debug`의 같은 네 파일: 새 oracle, 기존 후보 extension/history, + time/energy/algebra·state/writer28개. `.exit.txt=0`에도 N7 SKIP1개가 있다. +- `strict-physics`의 같은 네 파일: 보존 strict 바이너리 C08 실패 대조5개. + 초기 상대 baseline 경로의 launch 오류는 `strict-physics-initial-launch-error.txt`에 + 별도 기록했다. 이 launch는 native 실행이 아니며 검사 수에 넣지 않았다. +- `build-approved-release.raw.txt/.exit.txt`, `build-approved-debug.raw.txt/.exit.txt`: + 직접 MSBuild 재빌드 둘 다 exit0. 실제 VS 환경·target·probe 옵션은 + `build-final-release.cmd`, `build-final-debug.cmd`에 있다. +- `final-run-binary-hashes.json`, `verification.json`: 실행 전후 binary/source/DLL + SHA256, cwd/branch, 각 test identity·실패·skip, literal command와 제외 목록. + `summarize.py`와 `summary.raw.txt/.exit.txt`는 XML/filter/discovery 대조를 재현한다. + +최종 unit binary SHA256은 Release +`1dbb49aaacbb48ea483c4cba3b98805fa6b89f65cf4c61b428fc5e60f98b4492`, Debug +`248b23c0d2a1e3441424b6051fe5d54f648104e04fa22e3a403a41b680ded3d2`다. +보존 strict binary +`272a1829d6440dac32011dc30943bd8213b8bce5cbd4239243fb448f237be922`는 +`baseline/test-runtime-hashes.json`과 일치한다. inherited GTEST filter/shard를 제거하고 +`FESA_HHT_PROBE_OUTPUT`을 unset했으며 짧고 분리된 TMP/TEMP `C:/ft5f`, +`C:/ft5g`, `C:/ft5b`를 사용했다. + +## 정확도 실패와 strict 대조 + +최종 Release의 다음8개 identity는 FAIL이다. 수치 gate를 변경하여 합격시키지 않았다. + +| identity (suite 생략 표기) | 현재 실패 evidence | +| --- | --- | +| `HhtShellPhysics.C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum` | pointwise hidden 최대 `.0012337114403665182 > .001`; t=.0625 이후 여러 `CompleteEquilibrium` 실패 | +| `HhtShellPhysics.ReducedManufacturedControllerKeepsExactInitialOrbit` | h=1/64, increment1에서 strict outer100 소진 | +| `HhtShellPhysics.C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence` | 첫 grid h=1/16, increment1에서 strict outer100 소진 | +| `HhtShellPhysics.PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid` | h=1/256, increment1에서 strict outer100 소진 | +| `HhtShellPhysics.C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants` | 첫 grid h=1/64, increment1에서 strict outer100 소진 | +| `DynamicRotation.ThirdDerivativesMatchAllPermutationsAndInteriorSweep` | 독립 D3 norm error `.22761592213997162 > .001` | +| `DynamicRotation.MatchesEightyDigitIndependentBlockExponentialAnchors` | 독립 anchor error `.22761592213997159 > .001` | +| `NonlinearMitc4Residual.ExactDrillingConstraintHasIndependentForceGradientAndHessian` | gradient error `.0032913506131779374 > .001` | + +보존 strict 바이너리에서도 C08의 위5개가 모두 같은 hidden/equilibrium 및 같은 +첫 증분·grid의 correction budget 실패로 끝났다. `RunPlate`는 source상 자체 +strict residual/correction `1e-10`을 사용한다. 이 대조는 후보 field 정책 때문에 +C08 harness의 새 실패가 생겼다는 주장을 뒷받침하지 않으며, **후보 production의 +시간 정확도가 통과했다는 뜻도 아니다.** Local D3/drilling의 세 실패는 T2에서 이미 +기록한 local 근사의 부적합과 같고 현재 raw 결과도 남겼다. 본 태스크는 local 근사를 +수정하거나 physics 기준·kernel·HHT 계수·outer/controller를 바꿀 권한이 없다. + +T4의 동일 production SHA evidence는 추가로 재사용한다: +`../T4/final-native-{release,debug}-selected.xml` 각각86개 중84PASS/2FAIL 및 +`strict-baseline-related.xml`의2FAIL이다. T5의 수정은 test-only라 production SHA는 +불변이다. LargeDirect는 후보 field-pass 뒤 `dynamic-invalid-mass-null`, Uniform은 +첫 강체 병진 이후 finite-rotation increment2의 triad gate에서 실패했다. 보존 strict는 +두 검사에서 outer100 소진, 특히 Uniform의 첫 병진에서 막혀 후속 finite rotation에 +도달하지 않았다. 따라서 후속 triad 실패의 원인이 strict와 같다고 단정하지 않는다. +T4의 후보 output-failure rollback/accepted-once와 CLI old-final 보호 evidence도 +그 범위로만 사용하며 이를 새 시간수렴 또는 T7 전체 검사 통과로 세지 않는다. + +## 통과 범위와 남은 R8 + +현재 실행에서 partial rotation BC/null 교집합·exact-zero, physical rate 복원, +full/augmented tangent와 restricted defect, fixed chart/transpose·rank/regularity, +pair/RN-even·zero-scale·nonfinite 산술, lifecycle rollback, state/source identity, +schema2/writer의 endpoint/A 분리·부분 moment reaction·old-final 보호 검사가 통과했다. +실제 후보 연결은 새 강체 oracle 및 기존 extension/history/lifecycle 검사로, +각 operator·합성 writer fixture의 검증과 구분한다. + +C01/C03/C04a/C04b는 생산 시간 helper를 쓰지만 자체 strict Newton/algebra harness다. +아래는 최종 Release raw에서 추출한 q/v/physical-a의 finest normalized error 및 +마지막 두 refinement 차수의 channel별 최솟값이다. 모든 해당 channel은 감소했고 +차수 >=1.8, 기존 더 엄격한 finest1e-4도 만족했다. 이를 후보 field path의 R8로 +승격하지 않는다. 사용자 승인 finest1e-3을 적용하기 위해 기존 검사를 완화할 필요도 없었다. + +| algebra model | finest q / v / physical a | 최소 마지막 차수 q / v / a | +| --- | --- | --- | +| C01 |1.10353e-5 / 4.26133e-6 / 1.10353e-5|1.99948 / 1.99851 / 1.99948| +| C03 variable mass |1.10242e-6 / 3.42816e-7 / 9.26995e-7|1.99989 / 1.99477 / 1.99886| +| C04a coupled massless |5.26648e-6 / 4.20639e-6 / 1.05330e-5|1.99977 / 1.99472 / 1.99977| +| C04b moving null |1.62408e-6 / 1.66011e-6 / 3.16757e-6|1.99932 / 1.99962 / 2.00237| + +별도의 `HhtTemporalVerification.C04aFinestReductionComparisonIsNumericallyResolved`는 +Release/Debug 모두 SKIP이다. h=1/256 full/reduced acceleration defect +`3.0790209097630861e-10`이 arithmetic/solve resolution `6.2961354117696263e-10`보다 +작아 판정 불가다. C04a의 위 analytic 시간수렴 통과와 이 comparison SKIP을 섞지 않는다. +C07 energy·spectrum·reaction, C02 STEP oscillator, 비상수 a/A helper 검사는 통과했으나 +이는 실제 후보 nonlinear shell의 energy/시간수렴 closure를 대신하지 않는다. + +R8의 **실제 후보 nonlinear shell q/v/physical-a 마지막 두 refinement·order>=1.8· +finest<=1e-3·기존 N7 separation**은 아직 충족하지 못했다. C08 시간 검사는 첫 grid에서 +실패하여 마지막 refinement와 invariant 감소 판정까지 도달하지 않았다. 별도의 +유효한 비선형 production analytic oracle도 구성하지 않았으며, 미세 grid를 외부 truth로 +삼거나 선형 근사를 exact nonlinear truth로 대입하지 않았다. 실패/미평가를 PASS로 +세거나 hidden retry/polishing·failed-frame commit으로 성공을 만들지 않는다. + +현재 결과는 T6 장시간 성능 측정의 수치 승인 근거가 될 수 없다. 독립적으로 남은 +범위는 local 근사의 독립 정확도 부적합, candidate finite-rotation acceptance 실패, +candidate 비선형 시간수렴·N7/physical invariant closure다. 전체 Native Debug, +전체 CMake CTest, 별도 Python 및 필수 reference 검증은 T7의 별도 실행 결과를 따른다. diff --git a/tests/unit/analysis/hht_dynamic_procedure_test.cpp b/tests/unit/analysis/hht_dynamic_procedure_test.cpp index 1e97734..75594e9 100644 --- a/tests/unit/analysis/hht_dynamic_procedure_test.cpp +++ b/tests/unit/analysis/hht_dynamic_procedure_test.cpp @@ -750,6 +750,86 @@ TEST(HhtDynamicProcedure, AcceptedFluxHistoryCountsEachIncrementExactlyOnce) { EXPECT_EQ(n, 2U); } +TEST(HhtDynamicProcedure, CandidateRigidTranslationMatchesIndependentPhysicalOracle) { + // Two disconnected unit squares: rho=2, thickness=.1, area=1 each. + // Each nodal lump has mass .05 and receives F_x=1, hence a_x=20. + // This exact rigid motion verifies endpoint semantics, not temporal order. + constexpr double kNumericalTolerance = 1e-3; + constexpr double kAcceleration = 20.0; + constexpr double kTotalMass = 0.4; + constexpr double kTotalForce = 8.0; + constexpr double kLength = 1.0; + constexpr double kTime = 0.5; + constexpr double kEnergy = 20.0; + fesa::SerialParallelFor parallel; + for (const bool constrain_transverse : {false, true}) { + SCOPED_TRACE(constrain_transverse); + auto definition = MakeRigidTranslationDefinition(); + if (constrain_transverse) { + definition.dynamic_step->boundaries = { + {"All-A", 2, 6, 0.0, {"dynamic-state.inp", 30U}}, + {"All-B", 2, 6, 0.0, {"dynamic-state.inp", 31U}}}; + } + auto run = RunRigidTranslation(definition, parallel, + constrain_transverse ? "analytic-bc" : "analytic-free"); + ASSERT_TRUE(run.HasValue()) << (run.HasValue() ? "" : FailureText(run.GetStatus())); + const auto& frames = run.Value().execution.state.AcceptedFrames(); + ASSERT_EQ(frames.size(), 3U); + for (std::size_t index = 0U; index < frames.size(); ++index) { + const auto& frame = frames[index]; + EXPECT_DOUBLE_EQ(frame.time, static_cast(index) * 0.25); + const double q = 0.5 * kAcceleration * frame.time * frame.time; + const double v = kAcceleration * frame.time; + for (std::size_t node = 0U; node < 8U; ++node) { + const auto dof = 6U * node; + EXPECT_LE(std::abs(frame.generalized_coordinates[dof] - q) / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.velocity[dof] - v) * kTime / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.acceleration[dof] - kAcceleration) * + kTime * kTime / kLength, kNumericalTolerance); + EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof] - kAcceleration) * + kTime * kTime / kLength, kNumericalTolerance); + EXPECT_DOUBLE_EQ(frame.external_force[dof], 1.0); + EXPECT_LE(std::abs(frame.inertia_force[dof] - 1.0), kNumericalTolerance); + for (std::size_t component = 1U; component < 6U; ++component) { + const double scale = component < 3U ? kLength : 1.0; + EXPECT_LE(std::abs(frame.generalized_coordinates[dof + component]) / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.velocity[dof + component]) * kTime / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.acceleration[dof + component]) * kTime * kTime / scale, kNumericalTolerance); + EXPECT_LE(std::abs(frame.algorithmic_acceleration[dof + component]) * kTime * kTime / + scale, kNumericalTolerance); + if (constrain_transverse) { + EXPECT_DOUBLE_EQ(frame.generalized_coordinates[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.velocity[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.acceleration[dof + component], 0.0); + EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof + component], 0.0); + } + } + } + EXPECT_LE(std::abs(frame.kinetic_energy - 0.5 * kTotalMass * v * v) / + kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.external_work - kTotalForce * q) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.physical_strain_energy) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.drilling_energy) / kEnergy, kNumericalTolerance); + EXPECT_LE(std::abs(frame.energy_defect) / kEnergy, kNumericalTolerance); + for (const auto value : frame.global_equilibrium) + EXPECT_LE(std::abs(value), 1e-8); + } + std::size_t accepted = 0U; + std::istringstream input(run.Value().log); + std::string row; + while (std::getline(input, row)) { + const auto fields = LogFields(row); + if (fields.at("schema") != "hht-log-v2") continue; + EXPECT_EQ(fields.at("policy_id"), "abaqus2025-field-on-fesa-v1"); + if (fields.at("accepted") == "1") { + EXPECT_EQ(fields.at("field_pass"), "1"); + ++accepted; + } + } + EXPECT_EQ(accepted, 2U); + } +} + TEST(HhtDynamicProcedure, LoadFluxPreservesSourceCancellationAndExplicitZeroEntries) { auto definition = MakeDefinition(true); definition.dynamic_step->loads = {