diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43c96c7..3bf5dbf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,8 @@ jobs: dvidia-training-benchmark --help dvidia-skillspace-coverage --help dvidia-episode-tape --help + dvidia-demonstrate --help + dvidia-demonstration-export --help dvidia-reaction --help dvidia-motor-memory --help dvidia-observe --help @@ -80,6 +82,8 @@ jobs: dvidia-training-doctor --json - name: Verify pinned learning caches run: python -m unittest tests.test_camera_action_cache tests.test_camera_cache_cli tests.test_camera_action_export tests.test_camera_action_model -v + - name: Verify operator capture, recovery and reviewed exports without physics + run: python -m unittest tests.test_operator_capture tests.test_operator_export tests.test_godot_demonstration_bridge -v - name: Verify exact-reference and initial-head contracts without physics run: | python -B -m unittest tests.test_camera_action_reference_export -v diff --git a/README.md b/README.md index f027851..1a322d6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,15 @@ [Recorded demo](https://huggingface.co/spaces/Dvidia/dvidia-training) · [Synthetic datasets](https://huggingface.co/datasets/Dvidia/dvidia-training-examples) · [Pilot models](https://huggingface.co/Dvidia/dvidia-training-pilot) +**[Record paired demonstrations in Godot](docs/godot-demonstration-v02.md).** +Control the simulated PiPER's position and gripper, record synchronized native +RGB-D and issued actions, then export explicitly reviewed segments in Terminal. +Interrupted attempts and failed outcomes remain inspectable. A scripted windowed +check reproduced all **10,200 native scalar values exactly** in independent replay. +This is a capture/export prototype; human demonstrations, new policy training and +real-time teleoperation qualification remain ahead. +[Evidence and timing limits](benchmarks/godot-demonstration-v02/README.md). + **[Verified local learning cache](docs/camera-action-export-cache-v01.md).** The **0.1.3.dev5 preview** adds `dvidia-train cache` to reuse verified camera/action features with caller-pinned data, split and source identities. On one 1,000-sample diff --git a/benchmarks/godot-demonstration-v02/README.md b/benchmarks/godot-demonstration-v02/README.md new file mode 100644 index 0000000..846050c --- /dev/null +++ b/benchmarks/godot-demonstration-v02/README.md @@ -0,0 +1,89 @@ +# Godot demonstration capture: integration evidence + +This is a scripted recording/export check, not a manipulation benchmark or a +new learned skill. The final recording contains ten 50 Hz command intervals, +six 320x240 RGB-D pairs, and 200 native 1 ms ticks. It does not attempt a complete +placement; the independent task scorer correctly returns failure, with no +critical contact ticks. No human demonstrations or model fits were performed. + +The original evidence below is pinned to commit `b8b96cd`. Windows CI then +identified a local inventory serialization bug: backslash paths were correctly +rejected by the portable recording validator. The follow-up fix uses POSIX +paths for locally generated inventory entries, retaining strict validation of +imported references. `portability-tests.json` records **97 passing test executions** +including Windows/POSIX path cases. `portability-checks.json` records a fresh +windowed capture, exact replay of another 10,200 native values and a reinstall/ +export check after the fix. This second short run took **0.482349 wall seconds +for 0.2 simulated seconds**; it is a compatibility check, not a speedup claim. +Native rendering remains tested on Mac; cross-platform data tests do not +qualify Windows rendering. Original receipts are retained unchanged. + +## Results + +| Check | Result | +|---|---| +| Python regression tests, including existing capture/export contracts | 96 test executions passed | +| Godot coordinate and recording-contract assertions | 7 + 29 passed | +| Actual windowed Godot: Begin, ten steps, Finish | Passed; zero client errors | +| Independent native replay | All 10,200 scalar values exactly matched; maximum difference 0 | +| Independently reconstructed task score | Matched; task unsuccessful | +| Fresh installation outside checkout | Both CLI entry points work; installed source hashes match | +| Reviewed direct-action export and reload | 10 rows x 269 causal features, 10 x 7 issued target labels | +| Learner inputs | Existing feature contract; no teacher calls or native object/contact truth | + +Fault-injection tests cover interrupted intent/outcome writes, fsync before +in-memory journal cursor updates, and truncated trailing records. Finalization +reconstructs the committed prefix before sealing. A truncated journal remains +unsealed and recoverable. Pause/resume/reset/disconnect were also exercised over +real localhost TCP on an earlier source revision; those retained development +captures are not presented as evidence pinned to the final recorder source. +Their final-source behavior remains covered by the Python regressions above. + +## Timing scope + +Final windowed capture: **0.2 simulated seconds in 0.406678 wall seconds** after +the initial handshake. The server reports 201.570 ms aggregate command handling, +including controller, camera/perception, file writing and finalization. Twelve +RPC samples include Begin and Finish; their p95 is 53.432 ms. This small sample +does not establish a tail-latency bound, sustained throughput or 50 Hz real-time +operation. Physics, wall-clock delivery and GUI frame rate are different metrics. + +Host: Apple M5, 10 logical processors, macOS 26.5.2 arm64, Python 3.12.9, NumPy 2.5.3, +MuJoCo 3.15.0, Godot 4.7.2. The standalone viewer uses the compatibility OpenGL +renderer. No GPU-training speed, RAM minimum or physical reaction latency was +measured. Godot headless assertions passed with sandbox log/certificate-access +warnings; the actual windowed run completed without client errors. + +Earlier development windowed receipts measured 0.511076 and 0.595010 seconds +for the same short simulation duration, on earlier recorder/UI revisions. They +remain local alongside the initial graphics-sandbox refusal and a development +capture rejected after the source-identity contract changed. Do not infer a +controlled speedup from these development runs. + +## Evidence and reproduction + +- `tests.json` pins tested source and reports the regression executions. +- `windowed-client.json` retains the scripted request/state trace and client + source hashes. Its local output directory is replaced with a portable label; + other values match the original receipt. +- `native-replay.json` records independently executed native array comparisons. +- `capture-summary.json` retains the independent task score, source identities + and exact original capture inventory. The synthetic pixel/native capture is + retained locally; it is not bundled in this small source/evidence publication. +- `fresh-install.json` records installed source hashes and exported array shapes. +- `workstation.png` is the real final window screenshot, not a design mockup. + +Follow [the capture guide](../../docs/godot-demonstration-v02.md). To reproduce +the short engineering check, prepare with `--duration 0.2`, serve with +`--source-kind simulation_scripted_input --max-episodes 1`, then run Godot with +`--smoke-steps 10 --smoke-output /absolute/fresh/receipt.json` and an optional +`--screenshot /absolute/fresh/workstation.png`. Run the native replay script on +the resulting episode. Use fresh directories; failed attempts are preserved. + +MIT applies to the added DVIDIA source. Upstream arm notices remain under +`third_party/robot-models` and in prepared scene exports. MuJoCo is an external +Apache-2.0 dependency. No credentials, real footage or model weights are included. + +Next: collect actual operator demonstrations, review useful recovery segments, +and register a separate learning comparison on unseen starts. The existing +5/64 learned placement result and its unqualified status remain unchanged. diff --git a/benchmarks/godot-demonstration-v02/capture-summary.json b/benchmarks/godot-demonstration-v02/capture-summary.json new file mode 100644 index 0000000..e2a0a24 --- /dev/null +++ b/benchmarks/godot-demonstration-v02/capture-summary.json @@ -0,0 +1,356 @@ +{ + "schema": "dvidia.operator-integration-evidence/v1", + "source_kind": "simulation_scripted_input", + "status": "complete", + "control_intervals": 10, + "camera_pairs": 6, + "frame_bytes": 2003397, + "strict_success": false, + "score": { + "evidence_scope": "Recomputed simulator-authored numeric evidence; no physics rerun, source attestation or hardware sensing.", + "false_alarms": { + "reason": "Continuous residual policy has no declared discrete alarm events.", + "status": "not_applicable" + }, + "grip_events": [], + "grip_summary": { + "airborne_events": 0, + "loss_events": 0, + "qualification_latency_s": [], + "restored_events": 0, + "supported_events": 0, + "unresolved_events": 0 + }, + "native_diagnostics": { + "premature_open_jaw": { + "count": 0, + "intervals": [], + "maximum_s": 0.0, + "total_s": 0.0 + }, + "support_touchdown_before_destination": { + "count": 0, + "intervals": [], + "maximum_s": 0.0, + "total_s": 0.0 + } + }, + "native_metrics_mismatches": [], + "native_recomputed": { + "carry_complete": false, + "carry_contact_loss_events": 0, + "dwell_seconds": 0.0, + "first_success_time_seconds": null, + "grasp_seen": false, + "lift_dwell_seconds": 0.0, + "lift_seen": false, + "max_carry_contact_loss_seconds": 0.0, + "max_object_lift_m": 6.229478044517123e-11, + "max_pad_normal_force_n": 0.0, + "retention_lost": false, + "success": false, + "unexpected_contact_native_ticks": 0 + }, + "reason": "native_outcome_failed", + "restoration_scope": "Loaded contact restored for 100 ms after sustained loss; does not establish task recovery or airborne regrasp.", + "schema": "dvidia.piper-feedback-score/v1", + "strict_v01": { + "native_metrics_source": "simulator-authored episode record; not authenticated sensor input", + "native_outcome_success": false, + "native_outcome_valid": true, + "reason": "native_outcome_failed", + "reference_xy_m": [ + 0.35, + 0.08 + ], + "required_dwell_s": 0.25, + "required_lift_dwell_s": 0.1, + "sample_interval_s": 0.02, + "success": false, + "terminal_dwell_s": 0.0, + "terminal_reference_success": false, + "terminal_xy_error_m": 0.12083027970892407, + "trace_lift_dwell_s": 0.0, + "trajectory_complete": true + }, + "success": false, + "terminal_only": { + "dwell_s": 0.0, + "scope": "Terminal-only geometry/release/settling/dwell; excludes lift, carry and retention history.", + "success": false, + "terminal_xy_error_m": 0.12083027970892407 + }, + "valid": true + }, + "teacher_queries": 0, + "human_demonstrations": 0, + "training_runs": 0, + "physical_robot_ready": false, + "source_sha256": { + "action_demonstration.py": "439c7cce558571221660cd51e7dba1675203237706ea8ea0f97f2fcf9a6a24c5", + "camera_action_capture.py": "399f512a08087c72bfa37e382157a2250f413e423974b77b1bd3c0a911ff0a1e", + "operator_capture.py": "aa82570a0d1126af4c903713fc29676f0a27ef133677cc2db673c72677171833", + "piper_action_capture.py": "da037b2198cf7d2f3a9aa40fc19a4c0f5cc375a22c5dd75606e2c1856f6efc7f", + "piper_encoder_kinematics.py": "6606bb8aad04e5abb1e9b6d1a8ec628a4114acd167862167184fd4eef1d21c34", + "piper_feedback_env.py": "0dbae423231968a81bdeed52226d8dffaf8df622013ed3a360d7217148f1bcac", + "piper_feedback_score.py": "863a272828208c631de4cf57647e973eb3e4f5e99eb7a1eacb4eaa68dc42eb0e", + "piper_imitation_score.py": "0cd4663abfdd7f14f2f33d93e9a2bd45122b998d6160d45b8afcc880512cba2b", + "piper_rgbd_tracker.py": "43202cfcea404061a3a65124d6ad24aeeec5fae8ffaf863caa245f2f54c46c36", + "piper_task.py": "e66a44a8df4e7a8ee42e4f3866f889f8797cdfb8e2688a146e09b75d2c76664f" + }, + "original_capture_inventory": { + "capture.json": { + "bytes": 3939, + "sha256": "fc38a3400aad363c50766861d6a99ea86b18c4097c172e2537fa3cb83a849ed3" + }, + "commands.jsonl": { + "bytes": 10022, + "sha256": "8142c96e50b2f16d44af39e5968817a136b44d9e6faf1172c58abe59f6982bc7" + }, + "context.json": { + "bytes": 11970, + "sha256": "45d91c1413cf4422621c6f369fdf07e38af0a8a464bda81f6aca86d05d8abe32" + }, + "decisions/0000.json": { + "bytes": 1178, + "sha256": "10ce60595f11035d7775f8184bd48fca3c1e292f99e180a2ca4491aefe5954dd" + }, + "decisions/0001.json": { + "bytes": 1190, + "sha256": "9d0356f275f2df9fa851d4a0863cebdb59a831f62b3dcf5762004160cef87d3d" + }, + "decisions/0002.json": { + "bytes": 1186, + "sha256": "c6f10250bcb8f65b33d1c4160caf7184b0d4bdf2d2b5c87d94436e59d253d616" + }, + "decisions/0003.json": { + "bytes": 1183, + "sha256": "1e5b7a6f54310697d6948e3dcbf3614d0a26001a9176477b9e0bb04833578dca" + }, + "decisions/0004.json": { + "bytes": 1176, + "sha256": "06cd807edeb20ae775d11fc8eed0449620168cb554321db7854fee0c75910f11" + }, + "decisions/0005.json": { + "bytes": 1172, + "sha256": "88ac0fe79b1ba29e10c75d792e8f4e18a440d8aaa4fa8eec60605c3df4943806" + }, + "decisions/0006.json": { + "bytes": 1178, + "sha256": "8cd7e27fe5ec1c1e925199a1a5fa4e389460b1ecb01c4be5fb7f9909fec07d39" + }, + "decisions/0007.json": { + "bytes": 1175, + "sha256": "40f9be55e1ae949ba05f553cef601676844bfe16edd164964b7398c82f552401" + }, + "decisions/0008.json": { + "bytes": 1177, + "sha256": "843fe1fe49553089da75b07f0e681500f768fd618e976f6ee3d9b239cbf6a453" + }, + "decisions/0009.json": { + "bytes": 1175, + "sha256": "1ace428bc5f7348f3e9f0209c0388e5e3804e07c375b98199cad7aa7e5e38b78" + }, + "frames/depth-0000.npy": { + "bytes": 307328, + "sha256": "d982b222370893603aefedd2f5dd1c04a3a1d54f623be6c1b17eb91277895815" + }, + "frames/depth-0001.npy": { + "bytes": 307328, + "sha256": "b8e284558df274f137bfca1919eac1fb2a31e63dd6483ce4c8e9beb6a1fd4425" + }, + "frames/depth-0002.npy": { + "bytes": 307328, + "sha256": "2e0684d6b0b31187b72ed5f2f4430dee8537995649020b935443f4936a191d5f" + }, + "frames/depth-0003.npy": { + "bytes": 307328, + "sha256": "310dcc0fa858d13b3c4e4e92758f6990632d26fee777e3ba6f4880eb91688fa6" + }, + "frames/depth-0004.npy": { + "bytes": 307328, + "sha256": "67c750b90803ce24cff8ece6c815c799a4388ead2122c79f3e0f3e059277e1d3" + }, + "frames/depth-0005.npy": { + "bytes": 307328, + "sha256": "314838d99eee0b4dd3acb0aa3f6818eab9d24d748574d03208bfa2af35dfc137" + }, + "frames/rgb-0000.png": { + "bytes": 26744, + "sha256": "8ff2bb7e5acf584a0cbbaa5f58ee2b8ad213c7c66bc12d13d19cae6e0ee4a218" + }, + "frames/rgb-0001.png": { + "bytes": 26815, + "sha256": "7e33a413d35cb18c14aac35e4b314f9bdce8efb332cdcd3dbc41ea32107a817b" + }, + "frames/rgb-0002.png": { + "bytes": 26640, + "sha256": "128502d03fa8cf915deccebb15ced0921a76b1811f5034c1a73bd53fd2f4a855" + }, + "frames/rgb-0003.png": { + "bytes": 26603, + "sha256": "763e7f679d5b110c81852e5e03c7f06048565e3813d8651fc1c751394f94e866" + }, + "frames/rgb-0004.png": { + "bytes": 26407, + "sha256": "a7320e0a952f195d6421c4f46afd06ba0edf18950b930a440fd2f09adb4a2678" + }, + "frames/rgb-0005.png": { + "bytes": 26220, + "sha256": "0ae34fcd4c628985da159e8ad2d0a87329b15ce0374d518e18b161304fdfb7ef" + }, + "integrity.json": { + "bytes": 7059, + "sha256": "dcb6692a98487ba0d813dc451a22ee723d7a964f718272b94eb74f3c5b40d127" + }, + "native/0000.json": { + "bytes": 3060, + "sha256": "7dca80baba62ddd30a717527f99efd725d66970ba3d678189133f58a924aff0e" + }, + "native/0001.json": { + "bytes": 3344, + "sha256": "39379363829d0220ec73325b1b50fb693f98276a31867253acb509fcd69a3740" + }, + "native/0002.json": { + "bytes": 3338, + "sha256": "2b52f8ee938db4e30950abdaab3650618040bb4cc2e6c1753c6eaefc5eb7d7e0" + }, + "native/0003.json": { + "bytes": 3329, + "sha256": "0ad29bebdbeb1190e0ead9425e83b25989bd9111ef87320aec40ba6d444111a3" + }, + "native/0004.json": { + "bytes": 3338, + "sha256": "dfb93351971d06416763a6883c1871f375aa87110a93070d1911465f806c7d57" + }, + "native/0005.json": { + "bytes": 3305, + "sha256": "81b1b51e932ce8037c43137c4a9fb06fec1a4abe4f5eac3e8cb60c322b90ed45" + }, + "native/0006.json": { + "bytes": 3314, + "sha256": "b1f22c64fd627cce592a4526436bf41dd4af98203a333885730b2fc52765c22f" + }, + "native/0007.json": { + "bytes": 3303, + "sha256": "02f66419369214e456a6dea98f70324b36f0e170ecff277385f275e9229c4086" + }, + "native/0008.json": { + "bytes": 3324, + "sha256": "df35201fa3b6e2d733171acbcc426245b0d239dbef3a362e762f7d485bc1844a" + }, + "native/0009.json": { + "bytes": 3309, + "sha256": "e94cf82c68bdd771063620c11b38a9f3372b460c4f563f8b92086c38b23f0654" + }, + "native/0010.json": { + "bytes": 3315, + "sha256": "091ea5b1ef0e756a2c58e61893819b414cb5c405446b809e5d04997eac8c2ec7" + }, + "native.npz": { + "bytes": 52639, + "sha256": "9c6418c886c5f7cfab93dacbda42a032a84e023182f265551b73b54bc6674e14" + }, + "native_episode.json": { + "bytes": 35430, + "sha256": "23b63a45bbfb1f1900301be0e2b9883d875f2259fd0750148c44aed6e066b88a" + }, + "native_metadata.json": { + "bytes": 7953, + "sha256": "9ec23009b8b2322d77c898872bd2172ba107c04e28aeb0c11700c2bba07f9a3c" + }, + "perception/0000.json": { + "bytes": 2683, + "sha256": "39a4b66ff1ffa57d2e64cd587392529de7343bd22341233ee77a81e923c7a10c" + }, + "perception/0001.json": { + "bytes": 2779, + "sha256": "63dac1953d549e686dfa3f9649f804b16e7234a1e17e1a9f6df3729450c8a2f5" + }, + "perception/0002.json": { + "bytes": 2893, + "sha256": "cca914a804089cd1fb1a46340be20abffa705cc3d67dfa7971c6cd1e120b7b53" + }, + "perception/0003.json": { + "bytes": 2979, + "sha256": "f1d7a5b4b9c32b98de58fc0afd7e294955d8f8f0a78fab3d8e170f45896900c7" + }, + "perception/0004.json": { + "bytes": 2892, + "sha256": "38b3d9cd183d14b67bfa7df8bd84c640231f82f67262c6efee83f869ee29093f" + }, + "perception/0005.json": { + "bytes": 2977, + "sha256": "62d55732ce68ec33b1ca6f57f33d76120f5762a0ce91e15a61eba30bcc4938d7" + }, + "perception/0006.json": { + "bytes": 2892, + "sha256": "9e7fb3838096edeafa166e59a2c7fc295bc15ffa1cee4690160304819a183a13" + }, + "perception/0007.json": { + "bytes": 2976, + "sha256": "8179c95c4bf286bd045c7f3ee12939197b6117046f95c5c6655246fe1e837ca2" + }, + "perception/0008.json": { + "bytes": 2892, + "sha256": "bb83ca63333edddd769bd5168527167f5a4a5d20d0757a951d687db02a991ef9" + }, + "perception/0009.json": { + "bytes": 2977, + "sha256": "4ec1b68ac1bda5cc5fc32029b0a46d0cc836a881faa037f95fae11d0cccb207d" + }, + "perception/0010.json": { + "bytes": 2142, + "sha256": "e47c30dace4f1386f6d191865a39a68e3e878dd7a410ed2e2f691d73aa5d0c54" + }, + "recording.json": { + "bytes": 75741, + "sha256": "aa01f6901cbad1d7b850ec2587329f7de4b378f8f9fc49901df179396f1f0351" + }, + "started.json": { + "bytes": 608, + "sha256": "ecbccf894dc2fe56bf363e5ecc68a87c0a895b8aa66eaa352e6d16e8e2db2806" + }, + "transitions/0000.json": { + "bytes": 6484, + "sha256": "0aa4b1c72d0040d0c98324b58be1436cf09999cca994900514abd67cf74126b0" + }, + "transitions/0001.json": { + "bytes": 6823, + "sha256": "b1eff2fd05a357fbd99bf4acde5c2f2b20734938978c2256283e5097aed084bc" + }, + "transitions/0002.json": { + "bytes": 6889, + "sha256": "0da2a69d6ecc6787bafdf5f5ab2ba3bcaa61c635d0d67902dc3a097dfaa25f9c" + }, + "transitions/0003.json": { + "bytes": 6880, + "sha256": "dbbcdd08eb771ea1e52e19c2326fb04e23f286b3b4bc9161ff43acc7a0b6aa73" + }, + "transitions/0004.json": { + "bytes": 6878, + "sha256": "bac2304735fcb15ee72441e268c9e75809a2a82c114d93f56938746471c8f555" + }, + "transitions/0005.json": { + "bytes": 6848, + "sha256": "1ae6a693701754a18fdcd917009ba866a70c8f7cbd7cd69a6b4356bc726efcb8" + }, + "transitions/0006.json": { + "bytes": 6838, + "sha256": "b9a84987c02d3626c7780892a93d41f9618394a28bc5c799d4fe01dafa135e5e" + }, + "transitions/0007.json": { + "bytes": 6843, + "sha256": "63587c8e0feb02d77923246b6742702ee9ba80b2ac2056b35807d4cb429d6ea7" + }, + "transitions/0008.json": { + "bytes": 6856, + "sha256": "217d758a528535064abb176b3f23caef4d0c3daa285ca6bda9f250927799e3b7" + }, + "transitions/0009.json": { + "bytes": 6850, + "sha256": "6a932fd96f72f274cf4e1c621228bb7b074fc4ba4464e6a230a63bd965f66c36" + } + }, + "scope": "One 0.2 simulated second scripted integration capture. Original pixel/native recording retained locally; inventory binds its bytes. Reproduction scripts supplied. No learned skill or real-time qualification." +} diff --git a/benchmarks/godot-demonstration-v02/fresh-install.json b/benchmarks/godot-demonstration-v02/fresh-install.json new file mode 100644 index 0000000..f81f063 --- /dev/null +++ b/benchmarks/godot-demonstration-v02/fresh-install.json @@ -0,0 +1,25 @@ +{ + "schema": "dvidia.operator-fresh-install/v1", + "installed_source_matches": true, + "source_sha256": { + "godot_demonstration_bridge.py": "6ad0ef1b74b1b07748a02c5367f3bde48e4b3ea77950c4ecdf6e7e30b809e901", + "operator_capture.py": "aa82570a0d1126af4c903713fc29676f0a27ef133677cc2db673c72677171833", + "operator_export.py": "9ff7ac7eeae179df340b0035a982c1672e01bd51d56c1e2dbdfa2e5b0a5455a7", + "camera_action_model.py": "e9a9c4d1d0f16fc7c91cac3ca2e6560d0532370ab78a2af5a4ec84425c23932f" + }, + "feature_shape": [ + 10, + 269 + ], + "target_shape": [ + 10, + 7 + ], + "source_kind": "simulation_scripted_input", + "source_group_count": 1, + "python": "3.12.9", + "numpy": "2.5.3", + "cwd_outside_checkout": true, + "training_started": false, + "physical_robot_ready": false +} diff --git a/benchmarks/godot-demonstration-v02/native-replay.json b/benchmarks/godot-demonstration-v02/native-replay.json new file mode 100644 index 0000000..647e742 --- /dev/null +++ b/benchmarks/godot-demonstration-v02/native-replay.json @@ -0,0 +1 @@ +{"capture_integrity_sha256":"dcb6692a98487ba0d813dc451a22ee723d7a964f718272b94eb74f3c5b40d127","control_intervals":10,"exact_native_arrays":true,"fields":{"applied_jaw_target_m":{"exact":true,"max_abs_error":0.0,"shape":[200]},"arm_actuator_controls_rad":{"exact":true,"max_abs_error":0.0,"shape":[200,6]},"grasp_contact_counts":{"exact":true,"max_abs_error":0.0,"shape":[200,2]},"gripper_joint_position_m":{"exact":true,"max_abs_error":0.0,"shape":[200,2]},"joint_position_rad":{"exact":true,"max_abs_error":0.0,"shape":[200,6]},"native_tick":{"exact":true,"max_abs_error":0.0,"shape":[200]},"normal_rate_limited_jaw_target_m":{"exact":true,"max_abs_error":0.0,"shape":[200]},"object_angular_velocity_rad_s":{"exact":true,"max_abs_error":0.0,"shape":[200,3]},"object_linear_velocity_m_s":{"exact":true,"max_abs_error":0.0,"shape":[200,3]},"object_minus_tcp_local_m":{"exact":true,"max_abs_error":0.0,"shape":[200,3]},"object_position_m":{"exact":true,"max_abs_error":0.0,"shape":[200,3]},"object_quaternion_wxyz":{"exact":true,"max_abs_error":0.0,"shape":[200,4]},"pad_normal_forces_n":{"exact":true,"max_abs_error":0.0,"shape":[200,2]},"pulse_active":{"exact":true,"max_abs_error":0.0,"shape":[200]},"pulse_closing_tail":{"exact":true,"max_abs_error":0.0,"shape":[200]},"pulse_minimum_opening_m":{"exact":true,"max_abs_error":0.0,"shape":[200]},"requested_jaw_target_m":{"exact":true,"max_abs_error":0.0,"shape":[200]},"t_seconds":{"exact":true,"max_abs_error":0.0,"shape":[200]},"table_object_contact_count":{"exact":true,"max_abs_error":0.0,"shape":[200]},"tcp_position_m":{"exact":true,"max_abs_error":0.0,"shape":[200,3]},"tcp_quaternion_wxyz":{"exact":true,"max_abs_error":0.0,"shape":[200,4]},"unexpected_contact_count":{"exact":true,"max_abs_error":0.0,"shape":[200]}},"independent_score_matches":true,"maximum_abs_error":0.0,"native_values_compared":10200,"original_strict_success":false,"physical_robot_ready":false,"recording_sha256":"aa01f6901cbad1d7b850ec2587329f7de4b378f8f9fc49901df179396f1f0351","schema":"dvidia.operator-native-replay/v1","scope":"CPU native command replay including reset; excludes camera, Godot, fitting and hardware. Native parity is separate from successful task completion.","score":{"evidence_scope":"Recomputed simulator-authored numeric evidence; no physics rerun, source attestation or hardware sensing.","false_alarms":{"reason":"Continuous residual policy has no declared discrete alarm events.","status":"not_applicable"},"grip_events":[],"grip_summary":{"airborne_events":0,"loss_events":0,"qualification_latency_s":[],"restored_events":0,"supported_events":0,"unresolved_events":0},"native_diagnostics":{"premature_open_jaw":{"count":0,"intervals":[],"maximum_s":0.0,"total_s":0.0},"support_touchdown_before_destination":{"count":0,"intervals":[],"maximum_s":0.0,"total_s":0.0}},"native_metrics_mismatches":[],"native_recomputed":{"carry_complete":false,"carry_contact_loss_events":0,"dwell_seconds":0.0,"first_success_time_seconds":null,"grasp_seen":false,"lift_dwell_seconds":0.0,"lift_seen":false,"max_carry_contact_loss_seconds":0.0,"max_object_lift_m":6.229478044517123e-11,"max_pad_normal_force_n":0.0,"retention_lost":false,"success":false,"unexpected_contact_native_ticks":0},"reason":"native_outcome_failed","restoration_scope":"Loaded contact restored for 100 ms after sustained loss; does not establish task recovery or airborne regrasp.","schema":"dvidia.piper-feedback-score/v1","strict_v01":{"native_metrics_source":"simulator-authored episode record; not authenticated sensor input","native_outcome_success":false,"native_outcome_valid":true,"reason":"native_outcome_failed","reference_xy_m":[0.35,0.08],"required_dwell_s":0.25,"required_lift_dwell_s":0.1,"sample_interval_s":0.02,"success":false,"terminal_dwell_s":0.0,"terminal_reference_success":false,"terminal_xy_error_m":0.12083027970892407,"trace_lift_dwell_s":0.0,"trajectory_complete":true},"success":false,"terminal_only":{"dwell_s":0.0,"scope":"Terminal-only geometry/release/settling/dwell; excludes lift, carry and retention history.","success":false,"terminal_xy_error_m":0.12083027970892407},"valid":true},"simulated_seconds":0.20000000000000015,"source_kind":"simulation_scripted_input","teacher_queries":0,"wall_seconds":0.34696154098492116} diff --git a/benchmarks/godot-demonstration-v02/portability-checks.json b/benchmarks/godot-demonstration-v02/portability-checks.json new file mode 100644 index 0000000..d4de8e0 --- /dev/null +++ b/benchmarks/godot-demonstration-v02/portability-checks.json @@ -0,0 +1,534 @@ +{ + "schema": "dvidia.operator-portability-check/v1", + "operator_implementation_sha256": { + "action_demonstration.py": "439c7cce558571221660cd51e7dba1675203237706ea8ea0f97f2fcf9a6a24c5", + "camera_action_capture.py": "399f512a08087c72bfa37e382157a2250f413e423974b77b1bd3c0a911ff0a1e", + "operator_capture.py": "f1b5b5c5abbdf090342bb18df76e95f1fd518bd05fc5705aedb956f0ce081715", + "piper_action_capture.py": "da037b2198cf7d2f3a9aa40fc19a4c0f5cc375a22c5dd75606e2c1856f6efc7f", + "piper_encoder_kinematics.py": "6606bb8aad04e5abb1e9b6d1a8ec628a4114acd167862167184fd4eef1d21c34", + "piper_feedback_env.py": "0dbae423231968a81bdeed52226d8dffaf8df622013ed3a360d7217148f1bcac", + "piper_feedback_score.py": "863a272828208c631de4cf57647e973eb3e4f5e99eb7a1eacb4eaa68dc42eb0e", + "piper_imitation_score.py": "0cd4663abfdd7f14f2f33d93e9a2bd45122b998d6160d45b8afcc880512cba2b", + "piper_rgbd_tracker.py": "43202cfcea404061a3a65124d6ad24aeeec5fae8ffaf863caa245f2f54c46c36", + "piper_task.py": "e66a44a8df4e7a8ee42e4f3866f889f8797cdfb8e2688a146e09b75d2c76664f" + }, + "native_replay": { + "capture_integrity_sha256": "3b5ca185dc1ca7946064c5eb66d6b7e5dfa7b1f28cf87ffd8259e1d3b78d3ac4", + "control_intervals": 10, + "exact_native_arrays": true, + "fields": { + "applied_jaw_target_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "arm_actuator_controls_rad": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 6 + ] + }, + "grasp_contact_counts": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 2 + ] + }, + "gripper_joint_position_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 2 + ] + }, + "joint_position_rad": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 6 + ] + }, + "native_tick": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "normal_rate_limited_jaw_target_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "object_angular_velocity_rad_s": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 3 + ] + }, + "object_linear_velocity_m_s": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 3 + ] + }, + "object_minus_tcp_local_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 3 + ] + }, + "object_position_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 3 + ] + }, + "object_quaternion_wxyz": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 4 + ] + }, + "pad_normal_forces_n": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 2 + ] + }, + "pulse_active": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "pulse_closing_tail": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "pulse_minimum_opening_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "requested_jaw_target_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "t_seconds": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "table_object_contact_count": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + }, + "tcp_position_m": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 3 + ] + }, + "tcp_quaternion_wxyz": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200, + 4 + ] + }, + "unexpected_contact_count": { + "exact": true, + "max_abs_error": 0.0, + "shape": [ + 200 + ] + } + }, + "independent_score_matches": true, + "maximum_abs_error": 0.0, + "native_values_compared": 10200, + "original_strict_success": false, + "physical_robot_ready": false, + "recording_sha256": "6b03befc86be692505679f0d7685bba56ab4224db7ac5ddbd78422551d95d4e3", + "schema": "dvidia.operator-native-replay/v1", + "scope": "CPU native command replay including reset; excludes camera, Godot, fitting and hardware. Native parity is separate from successful task completion.", + "score": { + "evidence_scope": "Recomputed simulator-authored numeric evidence; no physics rerun, source attestation or hardware sensing.", + "false_alarms": { + "reason": "Continuous residual policy has no declared discrete alarm events.", + "status": "not_applicable" + }, + "grip_events": [], + "grip_summary": { + "airborne_events": 0, + "loss_events": 0, + "qualification_latency_s": [], + "restored_events": 0, + "supported_events": 0, + "unresolved_events": 0 + }, + "native_diagnostics": { + "premature_open_jaw": { + "count": 0, + "intervals": [], + "maximum_s": 0.0, + "total_s": 0.0 + }, + "support_touchdown_before_destination": { + "count": 0, + "intervals": [], + "maximum_s": 0.0, + "total_s": 0.0 + } + }, + "native_metrics_mismatches": [], + "native_recomputed": { + "carry_complete": false, + "carry_contact_loss_events": 0, + "dwell_seconds": 0.0, + "first_success_time_seconds": null, + "grasp_seen": false, + "lift_dwell_seconds": 0.0, + "lift_seen": false, + "max_carry_contact_loss_seconds": 0.0, + "max_object_lift_m": 6.229478044517123e-11, + "max_pad_normal_force_n": 0.0, + "retention_lost": false, + "success": false, + "unexpected_contact_native_ticks": 0 + }, + "reason": "native_outcome_failed", + "restoration_scope": "Loaded contact restored for 100 ms after sustained loss; does not establish task recovery or airborne regrasp.", + "schema": "dvidia.piper-feedback-score/v1", + "strict_v01": { + "native_metrics_source": "simulator-authored episode record; not authenticated sensor input", + "native_outcome_success": false, + "native_outcome_valid": true, + "reason": "native_outcome_failed", + "reference_xy_m": [ + 0.35, + 0.08 + ], + "required_dwell_s": 0.25, + "required_lift_dwell_s": 0.1, + "sample_interval_s": 0.02, + "success": false, + "terminal_dwell_s": 0.0, + "terminal_reference_success": false, + "terminal_xy_error_m": 0.12083027970892407, + "trace_lift_dwell_s": 0.0, + "trajectory_complete": true + }, + "success": false, + "terminal_only": { + "dwell_s": 0.0, + "scope": "Terminal-only geometry/release/settling/dwell; excludes lift, carry and retention history.", + "success": false, + "terminal_xy_error_m": 0.12083027970892407 + }, + "valid": true + }, + "simulated_seconds": 0.20000000000000015, + "source_kind": "simulation_scripted_input", + "teacher_queries": 0, + "wall_seconds": 0.3151376669993624 + }, + "fresh_install": { + "schema": "dvidia.operator-portability-install/v1", + "source_sha256": { + "operator_capture.py": "f1b5b5c5abbdf090342bb18df76e95f1fd518bd05fc5705aedb956f0ce081715", + "operator_export.py": "9ff7ac7eeae179df340b0035a982c1672e01bd51d56c1e2dbdfa2e5b0a5455a7" + }, + "installed_source_matches": true, + "feature_shape": [ + 10, + 269 + ], + "target_shape": [ + 10, + 7 + ], + "source_kind": "simulation_scripted_input", + "base_cli_only": true, + "native_graphics_in_existing_environment": true, + "training_started": false, + "physical_robot_ready": false + }, + "windowed": { + "applied_sim_time_seconds": 0.2, + "client_script_sha256": "98aac9a91f93a0389943289aab4f58982d6cc1e13170d5ec71d732c5b05f802f", + "errors": [], + "expected_model_sha256": "9c12b9437e4825685921a229129e2154a225e308593f227d17042852926e060f", + "godot": "4.7.2-stable (official)", + "initial_sim_time_seconds": 0.0, + "initial_state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0285001685693837, + 8.99494444381412e-07, + 0.406602785364861 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.370119754365555, + 0.00141459357745771, + 0.382651036416412 + ], + [ + 0.503934732048841, + -0.0335858519235436, + 0.364577255289232 + ], + [ + 0.50393494030113, + 0.0364141480752759, + 0.364577071719593 + ], + [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.70710528118436, + 0.0, + 0.0, + -0.707108281185553 + ], + [ + 0.499797817587225, + -0.499803939752559, + -0.500197982610706, + -0.500200101494439 + ], + [ + 0.113506973362852, + -0.113513180244746, + -0.697937502868791, + -0.697935646745461 + ], + [ + -5.14302108226427e-06, + -0.752944436272245, + 3.06932012868452e-06, + -0.658084094816764 + ], + [ + 0.532409609503324, + -0.532414623999283, + -0.46533454282896, + -0.465336909250043 + ], + [ + -0.658084094823863, + -2.77304662247931e-07, + -0.752944436287289, + 1.94854711149505e-06 + ], + [ + 0.532411855874789, + -0.532412377639585, + -0.465336531348717, + -0.4653349207318 + ], + [ + -0.532414636467881, + -0.532409597034671, + 0.465336894984113, + -0.465334557094891 + ], + [ + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.0, + 1.57, + -1.3485, + 0.0, + 0.0, + 0.0, + 0.035 + ], + "native_time": 0.5, + "object_position": [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + "position_error_m": 0.0, + "qpos": [ + 4.033464064055e-20, + 1.57, + -1.3485, + -2.85702112257035e-18, + -1.3639173671723e-16, + 9.79662157176411e-18, + 0.035, + -0.035, + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838, + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.489069666284522, + 0.00141419756548088, + 0.366584943104309 + ], + "tcp_quaternion": [ + 0.658084094823863, + 2.77304662247931e-07, + 0.752944436287289, + -1.94854711149505e-06 + ], + "time": 0.0 + }, + "interactive_wall_seconds": 0.482349, + "logical_processors": 10, + "ok": true, + "physics_authority": "MuJoCo only", + "process_frame_interval_ms": { + "p50": 8.0, + "p95": 9.17199999999998, + "samples": 58 + }, + "processor": "Apple M5", + "project_sha256": "d886d21c17931d0fef863e6f219cb9da10bc4b916d4cd4bbb70012f07f47e556", + "render_process_frames": 71, + "requested_steps": 10, + "reset_responses": 0, + "rpc_roundtrip_ms": { + "p50": 11.436, + "p95": 56.971, + "samples": 12 + }, + "scene_integrity_sha256": "603d2ccecd20b890e67a62357dbd811dcbbf33714a90c2fbe2164403fd5c598f", + "schema": "dvidia.godot-demonstration-smoke/v1", + "server_compute_total_ms": 183.420040935744, + "step_responses": 10, + "timing_scope": "Startup-inclusive wall time; ready-to-completion interactive wall; Godot process delta intervals (not GPU render time); socket request-to-parsed-response latency; server compute includes controller, native camera capture and disk writes; excludes IPC and Godot rendering. Samples capped at 10000 per series.", + "trace_limit": 256, + "wall_seconds": 1.388024 + }, + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "finished", + "steps": 10.0 + }, + "task_strict_success": false, + "source_kind": "simulation_scripted_input", + "native_capture_integrity_sha256": "3b5ca185dc1ca7946064c5eb66d6b7e5dfa7b1f28cf87ffd8259e1d3b78d3ac4", + "original_receipts_sha256": { + "windowed-portable-client.json": "20ca61532228acb74dda6777bb5c72d0a88a1a50f364644aaa1b054b4c071077", + "native-replay-portable.json": "1d672119fa7197a8b82a20c3e07e790b52181cd2f6e78ef76429e082847ec8f6", + "install-portable.json": "c7186086f77fd9751fa35550703d2d5cd79e4aaf2cd9259c1950f00e2dc227f3" + }, + "scope": "Short scripted compatibility check, no full placement or model fitting. Raw synthetic capture and full trace retained locally." +} diff --git a/benchmarks/godot-demonstration-v02/portability-tests.json b/benchmarks/godot-demonstration-v02/portability-tests.json new file mode 100644 index 0000000..6a4b9bf --- /dev/null +++ b/benchmarks/godot-demonstration-v02/portability-tests.json @@ -0,0 +1,24 @@ +{ + "schema": "dvidia.operator-portability-verification/v1", + "test_modules": [ + "tests.test_operator_capture", + "tests.test_operator_export", + "tests.test_godot_demonstration_bridge", + "tests.test_camera_action_capture", + "tests.test_camera_action_export", + "tests.test_godot_bridge", + "tests.test_action_demonstration" + ], + "test_executions": 97, + "passed": true, + "failures": 0, + "errors": 0, + "output": ".................................................................................................\n----------------------------------------------------------------------\nRan 97 tests in 2.740s\n\nOK\n", + "source_sha256": { + "src/dvidia_training/operator_capture.py": "f1b5b5c5abbdf090342bb18df76e95f1fd518bd05fc5705aedb956f0ce081715", + "tests/test_operator_capture.py": "a30bdc8522538e1f0cd32f77e0a0ac4c9f14a2f5bc4d04a9c84c683c7fae5ad3" + }, + "prior_evidence_commit": "b8b96cd60bff4eb4be7902b7d9d3d47596037e08", + "change_scope": "Serialize locally discovered inventory paths as POSIX; imported paths remain strict.", + "native_physics_executed_by_these_tests": false +} diff --git a/benchmarks/godot-demonstration-v02/tests.json b/benchmarks/godot-demonstration-v02/tests.json new file mode 100644 index 0000000..f4e5017 --- /dev/null +++ b/benchmarks/godot-demonstration-v02/tests.json @@ -0,0 +1,26 @@ +{ + "schema": "dvidia.operator-verification/v1", + "test_modules": [ + "tests.test_operator_capture", + "tests.test_operator_export", + "tests.test_godot_demonstration_bridge", + "tests.test_camera_action_capture", + "tests.test_camera_action_export", + "tests.test_godot_bridge", + "tests.test_action_demonstration" + ], + "test_executions": 96, + "passed": true, + "failures": 0, + "errors": 0, + "output": "................................................................................................\n----------------------------------------------------------------------\nRan 96 tests in 2.825s\n\nOK\n", + "source_sha256": { + "src/dvidia_training/operator_capture.py": "aa82570a0d1126af4c903713fc29676f0a27ef133677cc2db673c72677171833", + "src/dvidia_training/operator_export.py": "9ff7ac7eeae179df340b0035a982c1672e01bd51d56c1e2dbdfa2e5b0a5455a7", + "src/dvidia_training/godot_demonstration_bridge.py": "6ad0ef1b74b1b07748a02c5367f3bde48e4b3ea77950c4ecdf6e7e30b809e901", + "scripts/verify-operator-replay.py": "9acde85ef288f86cf16da08efeb7790af81a3c7cfec0aff3e4c5f104f94f6f2f", + "integrations/godot-demonstration/main.gd": "98aac9a91f93a0389943289aab4f58982d6cc1e13170d5ec71d732c5b05f802f", + "integrations/godot-demonstration/project.godot": "d886d21c17931d0fef863e6f219cb9da10bc4b916d4cd4bbb70012f07f47e556" + }, + "native_physics_executed_by_these_tests": false +} diff --git a/benchmarks/godot-demonstration-v02/windowed-client.json b/benchmarks/godot-demonstration-v02/windowed-client.json new file mode 100644 index 0000000..0317bfd --- /dev/null +++ b/benchmarks/godot-demonstration-v02/windowed-client.json @@ -0,0 +1,2727 @@ +{ + "applied_sim_time_seconds": 0.2, + "client_script_sha256": "98aac9a91f93a0389943289aab4f58982d6cc1e13170d5ec71d732c5b05f802f", + "errors": [], + "expected_model_sha256": "9c12b9437e4825685921a229129e2154a225e308593f227d17042852926e060f", + "godot": "4.7.2-stable (official)", + "initial_sim_time_seconds": 0.0, + "initial_state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0285001685693837, + 8.99494444381412e-07, + 0.406602785364861 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.370119754365555, + 0.00141459357745771, + 0.382651036416412 + ], + [ + 0.503934732048841, + -0.0335858519235436, + 0.364577255289232 + ], + [ + 0.50393494030113, + 0.0364141480752759, + 0.364577071719593 + ], + [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.70710528118436, + 0.0, + 0.0, + -0.707108281185553 + ], + [ + 0.499797817587225, + -0.499803939752559, + -0.500197982610706, + -0.500200101494439 + ], + [ + 0.113506973362852, + -0.113513180244746, + -0.697937502868791, + -0.697935646745461 + ], + [ + -5.14302108226427e-06, + -0.752944436272245, + 3.06932012868452e-06, + -0.658084094816764 + ], + [ + 0.532409609503324, + -0.532414623999283, + -0.46533454282896, + -0.465336909250043 + ], + [ + -0.658084094823863, + -2.77304662247931e-07, + -0.752944436287289, + 1.94854711149505e-06 + ], + [ + 0.532411855874789, + -0.532412377639585, + -0.465336531348717, + -0.4653349207318 + ], + [ + -0.532414636467881, + -0.532409597034671, + 0.465336894984113, + -0.465334557094891 + ], + [ + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.0, + 1.57, + -1.3485, + 0.0, + 0.0, + 0.0, + 0.035 + ], + "native_time": 0.5, + "object_position": [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + "position_error_m": 0.0, + "qpos": [ + 4.033464064055e-20, + 1.57, + -1.3485, + -2.85702112257035e-18, + -1.3639173671723e-16, + 9.79662157176411e-18, + 0.035, + -0.035, + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838, + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.489069666284522, + 0.00141419756548088, + 0.366584943104309 + ], + "tcp_quaternion": [ + 0.658084094823863, + 2.77304662247931e-07, + 0.752944436287289, + -1.94854711149505e-06 + ], + "time": 0.0 + }, + "interactive_wall_seconds": 0.406678, + "logical_processors": 10, + "ok": true, + "physics_authority": "MuJoCo only", + "process_frame_interval_ms": { + "p50": 2.737, + "p95": 5.0, + "samples": 146 + }, + "processor": "Apple M5", + "project_sha256": "d886d21c17931d0fef863e6f219cb9da10bc4b916d4cd4bbb70012f07f47e556", + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "local-session/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "finished", + "steps": 10.0 + }, + "render_process_frames": 152, + "requested_steps": 10, + "reset_responses": 0, + "rpc_roundtrip_ms": { + "p50": 9.59, + "p95": 53.432, + "samples": 12 + }, + "scene_integrity_sha256": "603d2ccecd20b890e67a62357dbd811dcbbf33714a90c2fbe2164403fd5c598f", + "schema": "dvidia.godot-demonstration-smoke/v1", + "server_compute_total_ms": 201.569751021452, + "step_responses": 10, + "timing_scope": "Startup-inclusive wall time; ready-to-completion interactive wall; Godot process delta intervals (not GPU render time); socket request-to-parsed-response latency; server compute includes controller, native camera capture and disk writes; excludes IPC and Godot rendering. Samples capped at 10000 per series.", + "trace": [ + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 0.0 + }, + "request": { + "sequence": 1, + "type": "begin" + }, + "rpc_roundtrip_ms": 6.204, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0285001685693837, + 8.99494444381412e-07, + 0.406602785364861 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.279938606266863, + -1.22010801876944e-06, + 0.394831449851912 + ], + [ + 0.370119754365555, + 0.00141459357745771, + 0.382651036416412 + ], + [ + 0.503934732048841, + -0.0335858519235436, + 0.364577255289232 + ], + [ + 0.50393494030113, + 0.0364141480752759, + 0.364577071719593 + ], + [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.70710528118436, + 0.0, + 0.0, + -0.707108281185553 + ], + [ + 0.499797817587225, + -0.499803939752559, + -0.500197982610706, + -0.500200101494439 + ], + [ + 0.113506973362852, + -0.113513180244746, + -0.697937502868791, + -0.697935646745461 + ], + [ + -5.14302108226427e-06, + -0.752944436272245, + 3.06932012868452e-06, + -0.658084094816764 + ], + [ + 0.532409609503324, + -0.532414623999283, + -0.46533454282896, + -0.465336909250043 + ], + [ + -0.658084094823863, + -2.77304662247931e-07, + -0.752944436287289, + 1.94854711149505e-06 + ], + [ + 0.532411855874789, + -0.532412377639585, + -0.465336531348717, + -0.4653349207318 + ], + [ + -0.532414636467881, + -0.532409597034671, + 0.465336894984113, + -0.465334557094891 + ], + [ + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.0, + 1.57, + -1.3485, + 0.0, + 0.0, + 0.0, + 0.035 + ], + "native_time": 0.5, + "object_position": [ + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838 + ], + "position_error_m": 0.0, + "qpos": [ + 4.033464064055e-20, + 1.57, + -1.3485, + -2.85702112257035e-18, + -1.3639173671723e-16, + 9.79662157176411e-18, + 0.035, + -0.035, + 0.300000035805163, + -0.0299998185193901, + 0.0798922445174838, + 0.999999999999999, + 1.67902365554781e-13, + -3.31279997277328e-14, + -8.1080055409147e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.489069666284522, + 0.00141419756548088, + 0.366584943104309 + ], + "tcp_quaternion": [ + 0.658084094823863, + 2.77304662247931e-07, + 0.752944436287289, + -1.94854711149505e-06 + ], + "time": 0.0 + }, + "step_compute_wall_seconds": 7.18749943189323e-05 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 1.0 + }, + "request": { + "jaw": 0.035, + "sequence": 2, + "target": [ + 0.489, + 0.001, + 0.367 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 9.59, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0286269816016938, + 5.95133718401385e-07, + 0.406590012870732 + ], + [ + 0.280073811800612, + -4.18755622307657e-06, + 0.39499933316494 + ], + [ + 0.280073811800612, + -4.18755622307657e-06, + 0.39499933316494 + ], + [ + 0.370252885736409, + 0.00141067108286848, + 0.382803462086439 + ], + [ + 0.50406446936543, + -0.0335908035465138, + 0.364707310517855 + ], + [ + 0.504065263890179, + 0.0364090584648402, + 0.364705980542946 + ], + [ + 0.300000035805163, + -0.0299998185193866, + 0.0798922445501823 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707101535743915, + 0.0, + 0.0, + -0.707112026590269 + ], + [ + 0.499906990994153, + -0.499918410437661, + -0.500083575920548, + -0.500090992075614 + ], + [ + 0.11325254355777, + -0.113266143903134, + -0.697977636207146, + -0.697976977684576 + ], + [ + -7.54762801003702e-06, + -0.752707978444665, + 8.29304416058116e-06, + -0.658354539028937 + ], + [ + 0.53244780221016, + -0.53245621370258, + -0.465285202275262, + -0.4652949590902 + ], + [ + -0.658019542476942, + 1.95749481338911e-06, + -0.753000850993174, + 1.08701256874671e-05 + ], + [ + 0.532445438686355, + -0.53245857729688, + -0.465292466089051, + -0.465287695179925 + ], + [ + -0.532460836267655, + -0.532443179650227, + 0.465289669245286, + -0.465290492035586 + ], + [ + 0.999999999999999, + 7.91783572674168e-14, + -1.56234331726645e-14, + -8.1080055408736e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.00067376242938471, + 1.57278998236533, + -1.3785, + 0.00035391900033238, + 0.03, + -0.00044326262045094, + 0.035 + ], + "native_time": 0.52, + "object_position": [ + 0.300000035805163, + -0.0299998185193866, + 0.0798922445501823 + ], + "position_error_m": 0.00053610990133981, + "qpos": [ + -1.05936548199137e-05, + 1.57044716019909, + -1.34966563739816, + 3.28930481664867e-06, + 0.00088993700185388, + -1.981600422288e-05, + 0.0349996119934981, + -0.0350002500356295, + 0.300000035805163, + -0.0299998185193866, + 0.0798922445501823, + 0.999999999999999, + 7.91783572674168e-14, + -1.56234331726645e-14, + -8.1080055408736e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.489200041207311, + 0.00140901535446746, + 0.366716973885477 + ], + "tcp_quaternion": [ + 0.658019542476942, + -1.95749481338911e-06, + 0.753000850993174, + -1.08701256874671e-05 + ], + "time": 0.02 + }, + "step_compute_wall_seconds": 0.00474541599396616 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 2.0 + }, + "request": { + "jaw": 0.035, + "sequence": 3, + "target": [ + 0.489, + 0.001, + 0.367 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 53.432, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0292117206537298, + 1.03469035732351e-07, + 0.406530377300983 + ], + [ + 0.280705376667317, + -8.81187261442693e-06, + 0.396004542348998 + ], + [ + 0.280705376667317, + -8.81187261442693e-06, + 0.396004542348998 + ], + [ + 0.370867872507541, + 0.00140455939193972, + 0.383686539354192 + ], + [ + 0.504654583717597, + -0.0335987212747432, + 0.365411637797861 + ], + [ + 0.504655861232432, + 0.0364012812021874, + 0.36540540280713 + ], + [ + 0.300000035805163, + -0.0299998185193849, + 0.0798922445657737 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707095721187872, + 0.0, + 0.0, + -0.707117841012234 + ], + [ + 0.500418244260536, + -0.500437871934818, + -0.499563746015813, + -0.499579403910132 + ], + [ + 0.111768768212841, + -0.111793849887374, + -0.698214963408376, + -0.69821611443176 + ], + [ + -1.65816607347393e-05, + -0.75131240116242, + 1.03554307610747e-05, + -0.659946721696057 + ], + [ + 0.532756375229171, + -0.532777530163302, + -0.464923062240755, + -0.464935795785229 + ], + [ + -0.657509503021183, + 2.23871770989573e-05, + -0.753446249823068, + 3.95374054386065e-05 + ], + [ + 0.53274011311177, + -0.532793792022592, + -0.464946258910285, + -0.464912597580638 + ], + [ + -0.532796052243562, + -0.532737852653444, + 0.464914570177168, + -0.464944286448229 + ], + [ + 0.999999999999999, + 3.71151732054937e-14, + -7.32147263822263e-15, + -8.1080055405847e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.0006762285352128, + 1.57283723870636, + -1.39432943835374, + 0.00042608316456459, + 0.06, + -0.00053065356165351, + 0.035 + ], + "native_time": 0.54, + "object_position": [ + 0.300000035805163, + -0.0299998185193849, + 0.0798922445657737 + ], + "position_error_m": 0.00099121963190112, + "qpos": [ + -2.70395132369352e-05, + 1.57250929321144, + -1.35596224025916, + -7.62209028380319e-06, + 0.00647868819811825, + -7.96244294816407e-05, + 0.0349992111304108, + -0.0350007916364866, + 0.300000035805163, + -0.0299998185193849, + 0.0798922445657737, + 0.999999999999999, + 3.71151732054937e-14, + -7.32147263822263e-15, + -8.1080055405847e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.489793133230023, + 0.00140094180100793, + 0.367438978029409 + ], + "tcp_quaternion": [ + 0.657509503021183, + -2.23871770989573e-05, + 0.753446249823068, + -3.95374054386065e-05 + ], + "time": 0.04 + }, + "step_compute_wall_seconds": 0.0513655839604326 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 3.0 + }, + "request": { + "jaw": 0.035, + "sequence": 4, + "target": [ + 0.489, + 0.001, + 0.367 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 9.6, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0298193100835146, + -6.38940868816998e-07, + 0.406467120177517 + ], + [ + 0.281371672134388, + -1.5630220121412e-05, + 0.397452619753872 + ], + [ + 0.281371672134388, + -1.5630220121412e-05, + 0.397452619753872 + ], + [ + 0.371532971875034, + 0.00139557125514606, + 0.385125616398494 + ], + [ + 0.505317238349191, + -0.033610930642671, + 0.366838988072615 + ], + [ + 0.505319747993318, + 0.0363889091574758, + 0.36682944819448 + ], + [ + 0.300000035805163, + -0.0299998185193873, + 0.0798922445737758 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707087175770247, + 0.0, + 0.0, + -0.707126386059279 + ], + [ + 0.50094727058682, + -0.500978942931641, + -0.499021140573319, + -0.499048927552816 + ], + [ + 0.109662134444711, + -0.109704103072042, + -0.698546354774799, + -0.698550081432269 + ], + [ + -2.08065661519418e-05, + -0.749326297355025, + 2.37021901693568e-05, + -0.662200950692086 + ], + [ + 0.53277671028514, + -0.532803688234868, + -0.464886937026602, + -0.464918640799753 + ], + [ + -0.657471826515367, + 3.12744576871064e-05, + -0.753479125375973, + 6.31119188334233e-05 + ], + [ + 0.532746690025101, + -0.532833708232437, + -0.464925901784232, + -0.464879672053628 + ], + [ + -0.532835968481311, + -0.532744429397424, + 0.46488164456379, + -0.464923929461867 + ], + [ + 0.999999999999999, + -1.0529022087227e-13, + 2.25742834529336e-14, + -8.1080051858038e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.00068165443622578, + 1.57314436340865, + -1.39581963530974, + 0.00083762909392562, + 0.0664786881981183, + -0.00100978903138328, + 0.035 + ], + "native_time": 0.56, + "object_position": [ + 0.300000035805163, + -0.0299998185193873, + 0.0798922445737758 + ], + "position_error_m": 0.00239909516735613, + "qpos": [ + -5.12090801667549e-05, + 1.57465247446722, + -1.36411413612583, + 3.9096714043632e-06, + 0.0125874069704359, + -0.00013881659081271, + 0.0349992129297327, + -0.0350006275660999, + 0.300000035805163, + -0.0299998185193873, + 0.0798922445737758, + 0.999999999999999, + -1.0529022087227e-13, + 2.25742834529336e-14, + -8.1080051858038e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.490456607059185, + 0.00138909169039944, + 0.368866162145588 + ], + "tcp_quaternion": [ + 0.657471826515367, + -3.12744576871064e-05, + 0.753479125375973, + -6.31119188334233e-05 + ], + "time": 0.06 + }, + "step_compute_wall_seconds": 0.00454095902387052 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 4.0 + }, + "request": { + "jaw": 0.035, + "sequence": 5, + "target": [ + 0.489, + 0.001, + 0.367 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 30.851, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0301111386282401, + -1.55079322426746e-06, + 0.406436269387151 + ], + [ + 0.281714308689634, + -2.40121566421349e-05, + 0.398973253472369 + ], + [ + 0.281714308689634, + -2.40121566421349e-05, + 0.398973253472369 + ], + [ + 0.371894526016105, + 0.00138459044051064, + 0.386785115225539 + ], + [ + 0.505705766387291, + -0.0336259597594482, + 0.368704388670075 + ], + [ + 0.505710320596847, + 0.0363734777409528, + 0.368694946018334 + ], + [ + 0.300000035805126, + -0.0299998185195589, + 0.0798922445776 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.7070766739446, + 0.0, + 0.0, + -0.707136887146642 + ], + [ + 0.501196695118947, + -0.501243168133914, + -0.498755738211679, + -0.498798429029047 + ], + [ + 0.107497258676292, + -0.107559997243633, + -0.698879708528549, + -0.698886499638591 + ], + [ + -1.4115970465775e-05, + -0.747280840794954, + 5.34187660970048e-05, + -0.664508346018288 + ], + [ + 0.532423697085796, + -0.532440784485719, + -0.465275847753465, + -0.465349549580764 + ], + [ + -0.658051525433935, + 1.98696751094775e-05, + -0.752972897433871, + 7.21755428551339e-05 + ], + [ + 0.532382323107892, + -0.532482160755896, + -0.465327747298391, + -0.46529764464607 + ], + [ + -0.532484419458894, + -0.532380063971709, + 0.465299618861115, + -0.465325773202714 + ], + [ + 0.999999999999999, + -6.0037451821797e-12, + 1.27324292058029e-12, + -8.1079802196943e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.00069241571106076, + 1.57336938420724, + -1.3962053653543, + 0.00125948500218385, + 0.0725874069704359, + -0.00147586201742806, + 0.035 + ], + "native_time": 0.58, + "object_position": [ + 0.300000035805126, + -0.0299998185195589, + 0.0798922445776 + ], + "position_error_m": 0.00415835423854161, + "qpos": [ + -8.09116846130893e-05, + 1.57568202750084, + -1.37131068718782, + 5.39178429775579e-05, + 0.0172151600934759, + -0.00018749359660484, + 0.0349994049457202, + -0.0350000333403492, + 0.300000035805126, + -0.0299998185195589, + 0.0798922445776, + 0.999999999999999, + -6.0037451821797e-12, + 1.27324292058029e-12, + -8.1079802196943e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.490843047362226, + 0.00137468293442635, + 0.370708732814916 + ], + "tcp_quaternion": [ + 0.658051525433935, + -1.98696751094775e-05, + 0.752972897433871, + -7.21755428551339e-05 + ], + "time": 0.0800000000000001 + }, + "step_compute_wall_seconds": 0.0294266249984503 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 5.0 + }, + "request": { + "jaw": 0.025, + "sequence": 6, + "target": [ + 0.5, + 0.026, + 0.342 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 5.62, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0302295285587888, + 9.13029901503005e-06, + 0.40642366696476 + ], + [ + 0.281867802542857, + 7.56713980038748e-05, + 0.400257068664157 + ], + [ + 0.281867802542857, + 7.56713980038748e-05, + 0.400257068664157 + ], + [ + 0.372082660008922, + 0.00151444675829495, + 0.388331638269817 + ], + [ + 0.5059622374329, + -0.0334230973562285, + 0.370675134882842 + ], + [ + 0.505934874437492, + 0.0365051857553913, + 0.370601013449405 + ], + [ + 0.300000035805063, + -0.02999981851986, + 0.0798922445789978 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707201714351874, + 0.0, + 0.0, + -0.707011835274185 + ], + [ + 0.501389042633652, + -0.501259134338323, + -0.498739691865802, + -0.498605082131049 + ], + [ + 0.105820121765199, + -0.105635596768445, + -0.699173169318857, + -0.699142404542593 + ], + [ + -0.00022467313761863, + -0.745566372585989, + -0.00044592961318185, + -0.666431192800675 + ], + [ + 0.531589554832743, + -0.531911188013896, + -0.466406412018472, + -0.465776869419712 + ], + [ + -0.659153102232377, + 0.0004964493391315, + -0.752008556259562, + 0.0002695698352868 + ], + [ + 0.531560850901095, + -0.531939849169358, + -0.466443673350255, + -0.465739582005335 + ], + [ + -0.531942104387148, + -0.531558594065754, + 0.465741560954852, + -0.466441697379586 + ], + [ + 0.999999999999999, + -1.12687842579006e-11, + 2.38967195811158e-12, + -8.1079441390343e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.0156112200931965, + 1.59312745852293, + -1.38824036995583, + -0.0287405149978162, + 0.06645059684348, + 0.0285241379825719, + 0.034 + ], + "native_time": 0.6, + "object_position": [ + 0.300000035805063, + -0.02999981851986, + 0.0798922445789978 + ], + "position_error_m": 0.0401779347006097, + "qpos": [ + 0.00027277221006016, + 1.57609973656866, + -1.37688065363196, + -0.00093157932632156, + 0.0194394674982354, + -0.00013270386767107, + 0.0349711936164169, + -0.0349571341324095, + 0.300000035805063, + -0.02999981851986, + 0.0798922445789978, + 0.999999999999999, + -1.12687842579006e-11, + 2.38967195811158e-12, + -8.1079441390343e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.491077738925289, + 0.00154433838117561, + 0.372603599607019 + ], + "tcp_quaternion": [ + 0.659153102232377, + -0.0004964493391315, + 0.752008556259562, + -0.0002695698352868 + ], + "time": 0.1 + }, + "step_compute_wall_seconds": 0.00406033301260322 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 6.0 + }, + "request": { + "jaw": 0.025, + "sequence": 7, + "target": [ + 0.5, + 0.026, + 0.342 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 25.549, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0305212599801016, + 5.20770180078405e-05, + 0.406392394840107 + ], + [ + 0.282180885500295, + 0.00047210466809523, + 0.401185787901898 + ], + [ + 0.282180885500295, + 0.00047210466809523, + 0.401185787901898 + ], + [ + 0.372427271619094, + 0.00203064940550061, + 0.389516717542689 + ], + [ + 0.506416859643844, + -0.0325819875299991, + 0.372351833783646 + ], + [ + 0.506270690086894, + 0.037030782401309, + 0.372070983790859 + ], + [ + 0.300000035804978, + -0.0299998185202578, + 0.079892244579499 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707698068091957, + 0.0, + 0.0, + -0.706514999429532 + ], + [ + 0.5019954992429, + -0.501165596462184, + -0.498833684631164, + -0.497994496696371 + ], + [ + 0.104977503707038, + -0.10381061431792, + -0.699446464252223, + -0.699269421407399 + ], + [ + -0.00100157952445639, + -0.74429027218814, + -0.00235589643483851, + -0.667851358699335 + ], + [ + 0.530347603806195, + -0.531795673872449, + -0.468521063338516, + -0.465201884772322 + ], + [ + -0.660242103398579, + 0.00212033740614086, + -0.751049394938809, + 0.00082184618603808 + ], + [ + 0.530492102412508, + -0.53165214030493, + -0.468361974892695, + -0.465361355788981 + ], + [ + -0.531654390988405, + -0.530489846797822, + 0.465363342877168, + -0.468360000526687 + ], + [ + 0.999999999999999, + -1.38979833836126e-11, + 2.94719275358367e-12, + -8.1079042532261e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.0154549549359366, + 1.5927858930779, + -1.39160630897124, + -0.034532379351101, + 0.0672564050830589, + 0.035406730490284, + 0.0329999999999999 + ], + "native_time": 0.62, + "object_position": [ + 0.300000035804978, + -0.0299998185202578, + 0.079892244579499 + ], + "position_error_m": 0.0408771615604711, + "qpos": [ + 0.00167735458514192, + 1.5771292573069, + -1.3817249707911, + -0.00466134076525013, + 0.0203551427868718, + 0.00060735539837788, + 0.0348243703242873, + -0.0347891196029839, + 0.300000035804978, + -0.0299998185202578, + 0.079892244579499, + 0.999999999999999, + -1.38979833836126e-11, + 2.94719275358367e-12, + -8.1079042532261e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.491467306156427, + 0.00221854171440273, + 0.374133747288841 + ], + "tcp_quaternion": [ + 0.660242103398579, + -0.00212033740614086, + 0.751049394938808, + -0.00082184618603808 + ], + "time": 0.12 + }, + "step_compute_wall_seconds": 0.0239029590156861 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 7.0 + }, + "request": { + "jaw": 0.025, + "sequence": 8, + "target": [ + 0.5, + 0.026, + 0.342 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 5.56, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0309065623276959, + 0.00011566550761452, + 0.406350614085402 + ], + [ + 0.282578326595707, + 0.00104828097035111, + 0.401844441138742 + ], + [ + 0.282578326595707, + 0.00104828097035111, + 0.401844441138742 + ], + [ + 0.372844531969953, + 0.0027830507296888, + 0.390354914888 + ], + [ + 0.506947716215446, + -0.0312857406348366, + 0.373561925603925 + ], + [ + 0.506643451035064, + 0.0377596309800154, + 0.373086162635925 + ], + [ + 0.300000035804874, + -0.0299998185207506, + 0.0798922445796805 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.708417153905061, + 0.0, + 0.0, + -0.705793975642363 + ], + [ + 0.502840603148948, + -0.50099688465327, + -0.499003127813562, + -0.497141154829091 + ], + [ + 0.104716475060703, + -0.102124717300482, + -0.699694606322142, + -0.699308558399553 + ], + [ + -0.00159579434168795, + -0.743350128701834, + -0.0045255438062052, + -0.668885310836352 + ], + [ + 0.529425185469345, + -0.531747133600563, + -0.47059331326765, + -0.464215351325341 + ], + [ + -0.661010607699446, + 0.00393062172319021, + -0.750365412759855, + 0.00112874063031541 + ], + [ + 0.529791446218412, + -0.531385500477217, + -0.470185452230221, + -0.464624702213995 + ], + [ + -0.53138774818806, + -0.52978919173499, + 0.464626697038552, + -0.470183480989555 + ], + [ + 0.999999999999999, + -1.54262258791929e-11, + 3.27127241118971e-12, + -8.1078548984857e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + 0.016277493086202, + 1.59307780009486, + -1.3942928923862, + -0.0382352236003634, + 0.0668183942582019, + 0.036124273869931, + 0.0319999999999999 + ], + "native_time": 0.64, + "object_position": [ + 0.300000035804874, + -0.0299998185207506, + 0.0798922445796805 + ], + "position_error_m": 0.0410893953141293, + "qpos": [ + 0.00371397904524986, + 1.57848957155195, + -1.38586846391674, + -0.00847007559026604, + 0.0210902261155015, + 0.00154773089205861, + 0.034542880319804, + -0.0345048008086781, + 0.300000035804874, + -0.0299998185207506, + 0.0798922445796805, + 0.999999999999999, + -1.54262258791929e-11, + 3.27127241118971e-12, + -8.1078548984857e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.491915236398997, + 0.00320344733655242, + 0.375215842477037 + ], + "tcp_quaternion": [ + 0.661010607699446, + -0.00393062172319021, + 0.750365412759855, + -0.00112874063031541 + ], + "time": 0.14 + }, + "step_compute_wall_seconds": 0.00392633298179134 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 8.0 + }, + "request": { + "jaw": 0.01, + "sequence": 9, + "target": [ + 0.5, + -0.014, + 0.377 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 25.489, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0316279873927258, + 0.00017055058937235, + 0.406270967100376 + ], + [ + 0.283309898125239, + 0.00151868694994693, + 0.402495965857396 + ], + [ + 0.283309898125239, + 0.00151868694994693, + 0.402495965857396 + ], + [ + 0.373573803991159, + 0.00339568002973474, + 0.391010728939648 + ], + [ + 0.507734964390868, + -0.0300559448352075, + 0.374269260068488 + ], + [ + 0.507315502234998, + 0.0382134998135181, + 0.373711752812047 + ], + [ + 0.30000003580475, + -0.0299998185213326, + 0.0798922445797462 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.708999468214857, + 0.0, + 0.0, + -0.705209014456743 + ], + [ + 0.503883005764179, + -0.501220508808913, + -0.498778509510318, + -0.496084586035543 + ], + [ + 0.104277852838843, + -0.100530567275741, + -0.699925428201617, + -0.699374098324581 + ], + [ + -0.00175078620312932, + -0.742367949298123, + -0.00591401927917163, + -0.669964019167101 + ], + [ + 0.529273770798363, + -0.531847385138568, + -0.471593097510588, + -0.463257579372985 + ], + [ + -0.661040934148634, + 0.00500407316803607, + -0.750332443517145, + 0.00103287812458441 + ], + [ + 0.529835918257024, + -0.531294402560158, + -0.470965939500518, + -0.463887099802716 + ], + [ + -0.531296650459682, + -0.529833664160097, + 0.463889097938607, + -0.470963971389228 + ], + [ + 0.999999999999999, + -1.77892221551969e-11, + 3.77235960607874e-12, + -8.1077847592021e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.0137225069137979, + 1.60421007865189, + -1.4242928923862, + -0.00823522360036335, + 0.0810902261155015, + 0.00612427386993095, + 0.0309999999999999 + ], + "native_time": 0.66, + "object_position": [ + 0.30000003580475, + -0.0299998185213326, + 0.0798922445797462 + ], + "position_error_m": 0.0194803259317423, + "qpos": [ + 0.00536476017278135, + 1.58103683801668, + -1.3913208981431, + -0.0105758813524648, + 0.0239155453000586, + 0.00237610477136688, + 0.0341374417945607, + -0.0341355677741591, + 0.30000003580475, + -0.0299998185213326, + 0.0798922445797462, + 0.999999999999999, + -1.77892221551969e-11, + 3.77235960607874e-12, + -8.1077847592021e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.492644915044592, + 0.00400372685972205, + 0.375881231540412 + ], + "tcp_quaternion": [ + 0.661040934148634, + -0.00500407316803607, + 0.750332443517145, + -0.00103287812458441 + ], + "time": 0.16 + }, + "step_compute_wall_seconds": 0.0231787919765338 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "recording", + "steps": 9.0 + }, + "request": { + "jaw": 0.01, + "sequence": 10, + "target": [ + 0.5, + -0.014, + 0.377 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 5.229, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0330630393494723, + 0.00016171293911871, + 0.406107062352568 + ], + [ + 0.284760965174207, + 0.00138414866952468, + 0.403555184140099 + ], + [ + 0.284760965174207, + 0.00138414866952468, + 0.403555184140099 + ], + [ + 0.375009845398444, + 0.00321335189712374, + 0.391944839353081 + ], + [ + 0.509120320293985, + -0.029810036807199, + 0.37496038004465 + ], + [ + 0.508754435162816, + 0.0375299145673385, + 0.374511941934538 + ], + [ + 0.300000035804625, + -0.029999818521925, + 0.0798922445797699 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.708823229195373, + 0.0, + 0.0, + -0.705386156479587 + ], + [ + 0.505014617615303, + -0.502606912864632, + -0.497381434254118, + -0.494932557016478 + ], + [ + 0.102403393328511, + -0.0990045581398797, + -0.700142912174027, + -0.699651016603854 + ], + [ + -0.00150651496386381, + -0.74074279275994, + -0.00525631447974212, + -0.671766489596614 + ], + [ + 0.529772214622521, + -0.532012483850095, + -0.470754146367437, + -0.463351541833484 + ], + [ + -0.660513974400927, + 0.00423889274473392, + -0.750801454270339, + 0.00070546079580858 + ], + [ + 0.530399078191383, + -0.531394523040606, + -0.470052258795777, + -0.464055549193376 + ], + [ + -0.531396773329415, + -0.530396823669676, + 0.464057543452845, + -0.470050289969825 + ], + [ + 0.999999999999999, + -1.91757711603471e-11, + 4.06638981686116e-12, + -8.107706395839e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.0174756551801622, + 1.60440592614704, + -1.4513208981431, + 0.0217647763996366, + 0.0839155453000586, + -0.023875726130069, + 0.0299999999999999 + ], + "native_time": 0.68, + "object_position": [ + 0.300000035804625, + -0.029999818521925, + 0.0798922445797699 + ], + "position_error_m": 0.0187287818096827, + "qpos": [ + 0.00486500227711136, + 1.58610407031877, + -1.40124769989448, + -0.00933489195463268, + 0.0301803876083287, + 0.00265568028127004, + 0.0336377183412144, + -0.033704720134642, + 0.300000035804625, + -0.029999818521925, + 0.0798922445797699, + 0.999999999999999, + -1.91757711603471e-11, + 4.06638981686116e-12, + -8.107706395839e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.494059882769519, + 0.00375833296492821, + 0.376648026038486 + ], + "tcp_quaternion": [ + 0.660513974400927, + -0.00423889274473392, + 0.750801454270339, + -0.00070546079580858 + ], + "time": 0.18 + }, + "step_compute_wall_seconds": 0.00432241702219471 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "finished", + "steps": 10.0 + }, + "request": { + "jaw": 0.01, + "sequence": 11, + "target": [ + 0.5, + -0.014, + 0.377 + ], + "type": "step" + }, + "rpc_roundtrip_ms": 53.056, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0348919103263092, + 8.85923758895418e-05, + 0.405887515138388 + ], + [ + 0.286603290077862, + 0.00071953394569287, + 0.404973451516446 + ], + [ + 0.286603290077862, + 0.00071953394569287, + 0.404973451516446 + ], + [ + 0.376854594680505, + 0.0023497511876582, + 0.393352312092317 + ], + [ + 0.510868997132641, + -0.0304026631669414, + 0.376199853795774 + ], + [ + 0.510690265353495, + 0.0358406693348, + 0.37603218763886 + ], + [ + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707993860810236, + 0.0, + 0.0, + -0.706218587304962 + ], + [ + 0.506030967357756, + -0.504793396814417, + -0.495162222440851, + -0.493893369134445 + ], + [ + 0.0993040080966442, + -0.0975492118303743, + -0.700347164819901, + -0.700097646029425 + ], + [ + -0.00041927687469539, + -0.738568382049827, + -0.00230808468231853, + -0.674174489274329 + ], + [ + 0.530625632599135, + -0.531276509109516, + -0.468626497683904, + -0.465371802503861 + ], + [ + -0.660436683391414, + 0.0018487572903669, + -0.750879461574042, + 5.92765350540443e-05 + ], + [ + 0.530911158151972, + -0.530992760218895, + -0.468307528294082, + -0.465690980896065 + ], + [ + -0.530995012680279, + -0.530908905334796, + 0.465692967753252, + -0.468305552529593 + ], + [ + 0.999999999999999, + -1.97095690863489e-11, + 4.17957951805174e-12, + -8.1076203718985e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.0190766571955133, + 1.60658892065518, + -1.46124769989448, + 0.0506651080453673, + 0.0901803876083287, + -0.0538757261300691, + 0.0289999999999998 + ], + "native_time": 0.700000000000001, + "object_position": [ + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786 + ], + "position_error_m": 0.0171604923329814, + "qpos": [ + 0.00251485916982425, + 1.59256555135791, + -1.41421599979193, + -0.00374339604922114, + 0.0368930668518755, + 0.00120941144102162, + 0.0330703605258883, + -0.0331734252808144, + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786, + 0.999999999999999, + -1.97095690863489e-11, + 4.17957951805174e-12, + -8.1076203718985e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.495902377383542, + 0.00263217640043909, + 0.378030870597156 + ], + "tcp_quaternion": [ + 0.660436683391414, + -0.0018487572903669, + 0.750879461574042, + -5.92765350540442e-05 + ], + "time": 0.2 + }, + "step_compute_wall_seconds": 0.0521003330359235 + }, + { + "recording": { + "episode": 1.0, + "max_episodes": 1.0, + "max_steps": 10.0, + "output_directory": "/Users/roguedev/dvidia.org/dvidia-godot-demonstration-20261110/runs/operator-diagnostic/windowed-reviewed/episode-0001", + "physical_robot_ready": false, + "source_authenticated": false, + "source_kind": "simulation_scripted_input", + "status": "finished", + "steps": 10.0 + }, + "request": { + "sequence": 12, + "type": "finish" + }, + "rpc_roundtrip_ms": 2.038, + "state": { + "body_positions": [ + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.0 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0, + 0.0, + 0.123 + ], + [ + 0.0348919103263092, + 8.85923758895418e-05, + 0.405887515138388 + ], + [ + 0.286603290077862, + 0.00071953394569287, + 0.404973451516446 + ], + [ + 0.286603290077862, + 0.00071953394569287, + 0.404973451516446 + ], + [ + 0.376854594680505, + 0.0023497511876582, + 0.393352312092317 + ], + [ + 0.510868997132641, + -0.0304026631669414, + 0.376199853795774 + ], + [ + 0.510690265353495, + 0.0358406693348, + 0.37603218763886 + ], + [ + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786 + ], + [ + 0.35, + 0.08, + 0.0605 + ] + ], + "body_quaternions": [ + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ], + [ + 0.707993860810236, + 0.0, + 0.0, + -0.706218587304962 + ], + [ + 0.506030967357756, + -0.504793396814417, + -0.495162222440851, + -0.493893369134445 + ], + [ + 0.0993040080966442, + -0.0975492118303743, + -0.700347164819901, + -0.700097646029425 + ], + [ + -0.00041927687469539, + -0.738568382049827, + -0.00230808468231853, + -0.674174489274329 + ], + [ + 0.530625632599135, + -0.531276509109516, + -0.468626497683904, + -0.465371802503861 + ], + [ + -0.660436683391414, + 0.0018487572903669, + -0.750879461574042, + 5.92765350540443e-05 + ], + [ + 0.530911158151972, + -0.530992760218895, + -0.468307528294082, + -0.465690980896065 + ], + [ + -0.530995012680279, + -0.530908905334796, + 0.465692967753252, + -0.468305552529593 + ], + [ + 0.999999999999999, + -1.97095690863489e-11, + 4.17957951805174e-12, + -8.1076203718985e-19 + ], + [ + 1.0, + 0.0, + 0.0, + 0.0 + ] + ], + "ctrl": [ + -0.0190766571955133, + 1.60658892065518, + -1.46124769989448, + 0.0506651080453673, + 0.0901803876083287, + -0.0538757261300691, + 0.0289999999999998 + ], + "native_time": 0.700000000000001, + "object_position": [ + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786 + ], + "position_error_m": 0.0171604923329814, + "qpos": [ + 0.00251485916982425, + 1.59256555135791, + -1.41421599979193, + -0.00374339604922114, + 0.0368930668518755, + 0.00120941144102162, + 0.0330703605258883, + -0.0331734252808144, + 0.3000000358045, + -0.0299998185225119, + 0.0798922445797786, + 0.999999999999999, + -1.97095690863489e-11, + 4.17957951805174e-12, + -8.1076203718985e-19 + ], + "scope": "Viewer state includes native truth; excluded from learning inputs.", + "tcp_position": [ + 0.495902377383542, + 0.00263217640043909, + 0.378030870597156 + ], + "tcp_quaternion": [ + 0.660436683391414, + -0.0018487572903669, + 0.750879461574042, + -5.92765350540442e-05 + ], + "time": 0.2 + }, + "step_compute_wall_seconds": 9.17003490030766e-07 + } + ], + "trace_limit": 256, + "wall_seconds": 1.234756 +} diff --git a/benchmarks/godot-demonstration-v02/workstation.png b/benchmarks/godot-demonstration-v02/workstation.png new file mode 100644 index 0000000..0742782 Binary files /dev/null and b/benchmarks/godot-demonstration-v02/workstation.png differ diff --git a/docs/godot-demonstration-v02.md b/docs/godot-demonstration-v02.md new file mode 100644 index 0000000..cb92b34 --- /dev/null +++ b/docs/godot-demonstration-v02.md @@ -0,0 +1,132 @@ +# Record demonstrations locally + +This prototype connects a new Godot demonstration UI to the existing PiPER +rigid-block learning scene. A person can command XYZ and gripper opening, +record paired observations/actions, inspect the attempt and export selected +rows for the existing CPU learner. The tool orientation is fixed. + +This release implements capture and export. It does not add hand tracking, +physical teleoperation, a Train button, a newly trained policy or a qualified +skill. The existing learned placement result remains 5/64. The older Godot +rigid-shoe workstation and all previous study evidence remain unchanged. + +## Run + +Use Python 3.12+, the pinned NumPy/MuJoCo dependencies, Godot 4.7.2 and a working +native graphics context. The tested host is Apple M5/macOS; other platforms +require their own rendering checks. Installation alone does not include Godot +or the separately licensed arm assets. Keep the checkout for the Godot project. + +```sh +python -m venv .venv +source .venv/bin/activate +python -m pip install '.[arm]' +dvidia-robot-sim fetch piper --output ./robot-assets +dvidia-demonstrate prepare --assets ./robot-assets --output ./demo-scene +dvidia-demonstrate serve --assets ./robot-assets --scene ./demo-scene --output ./demo-session +``` + +In a second terminal, replace the scene path with your absolute directory: + +```sh +godot --path integrations/godot-demonstration -- --scene /absolute/path/to/demo-scene --port 8431 +``` + +Choose the target and press **Record**. Move the XYZ and gripper controls; +**Pause** holds simulation time, **Finish** seals the current prefix, and +**New episode** preserves the previous recording and resets into a new directory. +Home changes the target, without resetting or erasing the recording. The +default budget is four episodes, each at most 20 simulated seconds. Finishing +early produces an interrupted recording; recording completion and task success +are separate. Closing the client retains the committed prefix. + +The service listens only on localhost, accepts one client and exits on its +disconnect. No physical commands or cloud service are used. Provisioning the +dependencies/assets requires networking; subsequent capture and export are local. +The capacity guard reserves 10 GiB plus the declared capture budget. This small +capture service does not override the older full study's separate disk guard. + +Use `prepare --case case.json` for explicit starts and either existing destination. +The file has `id`, `group_id`, `seed`, `object_position_m`, and `goal_position_m`. +Changing seed alone does not change the object position. The default case is +the original block at [0.30, -0.03, 0.082] and goal [0.35, 0.08, 0.08] metres. +Keep related starts, retries and corrections in the same train/development/test +group. A different case needs its own prepared scene and session. + +## Inspect and export + +```sh +dvidia-demonstrate inspect ./demo-session/episode-0001 +dvidia-demonstration-export --capture ./demo-session/episode-0001 --review ./review.json --output ./reviewed-data +``` + +Example `review.json`: + +```json +{"segments": [[0, 50]], "reason": "Reviewed useful approach segment, before the unsuccessful grasp."} +``` + +Segments are half-open control-row ranges; `[0, 50]` selects one simulated second. +Alternatively supply `indices` and `reason`. Duplicate or overlapping selections +are rejected. Selection is an unauthenticated review assertion, not an automatic +quality certification. Preserve failures, but only select actions that are useful +for the intended learning experiment. The exporter does not train automatically. + +`operator_export.load_dataset(path)` returns the existing camera-action dataset +contract, suitable for its Python training API. It verifies the saved arrays and +provenance. UI/Terminal captures use the same data; the existing fixed-budget +study orchestrator does not silently accept these as authored-teacher episodes. +A new human-demonstration study still needs a registered split, fit budget and +held-out evaluation before a model can be presented as a useful skill. + +After an abrupt process loss, `dvidia-demonstrate recover PATH` reads the +durable committed prefix without changing the directory. It never resumes an +uncertain native interval, fabricates task success or produces a training export +from an unsealed attempt. `inspect` is for normally sealed attempts. + +## Data and clocks + +- MuJoCo alone advances physics: twenty 1 ms ticks per 50 Hz control interval. +- Native RGB-D is 320x240 at 25 Hz, with calibration and frame hashes. The Godot + orbit view is operator visualization, not the learner's camera. +- Missing perception stays missing. Historical observations retain acquisition + times and masks. Future frames, native object/contact truth and current action + labels are excluded from learner inputs. +- The journal distinguishes input requests, the issued seven-channel command, + the final applied reference and measured joints. The gripper command is one + finger-slide target in metres, not measured full aperture or force. +- A separate encoder-only FK/IK component maps the input target to joint + commands. It receives measured joints, target and jaw, never object/contact + truth. Original limits and native rate limits remain active. +- Native evidence independently checks grasp/lift/retention/release/settling/ + retreat. A successful recording can contain a failed task. + +The source kind is `simulation_human_input` by default, a producer declaration +with `human_authenticated=False`. Automated checks explicitly use +`simulation_scripted_input`. Neither is a physical teleoperation recording. +Every package remains `physical_robot_ready=False`. + +Simulation timestamps and monotonic wall timestamps are separate. A slow camera +or disk slows simulation progress; it does not skip physics ticks. The software +does not establish a 50 Hz wall-clock deadline or real-robot reaction latency. +Capture includes durable writes, rendering and perception costs that the older +unrendered throughput benchmark excluded. + +## Verify + +```sh +python -m unittest tests.test_operator_capture tests.test_operator_export tests.test_godot_demonstration_bridge -v +godot --headless --path integrations/godot-demonstration --script tests/test_coordinates.gd +godot --headless --path integrations/godot-demonstration --script tests/test_recording.gd +python scripts/verify-operator-replay.py --assets ./robot-assets --capture ./demo-session/episode-0001 --output ./native-replay.json +``` + +The native replay script independently executes issued commands and compares +dense native arrays plus the independently reconstructed task score. It measures +reproduction of simulation behavior, not successful manipulation or sim-to-real. +Use `--source-kind simulation_scripted_input` when serving a scripted Godot smoke +or TCP lifecycle check; never label those runs human demonstrations. + +No new learning result is claimed by this release. The next milestone is genuine +operator recordings across starts and goals, followed by the bounded, separately +evaluated learning experiment. diff --git a/integrations/godot-demonstration/.gitignore b/integrations/godot-demonstration/.gitignore new file mode 100644 index 0000000..0f10302 --- /dev/null +++ b/integrations/godot-demonstration/.gitignore @@ -0,0 +1 @@ +.godot/ diff --git a/integrations/godot-demonstration/README.md b/integrations/godot-demonstration/README.md new file mode 100644 index 0000000..dd94371 --- /dev/null +++ b/integrations/godot-demonstration/README.md @@ -0,0 +1,57 @@ +# DVIDIA Demonstration studio + +A standalone Godot 4.7 project for recording simulated PiPER block demonstrations. It renders the native MuJoCo geometry and exposes target X/Y/Z and the single-finger jaw target. Tool orientation is fixed. The existing `integrations/godot` workstation is a separate project. + +Prepare the matching scene, start the local recorder, then open the viewer. Use fresh output directories: + +```sh +python -m dvidia_training.godot_demonstration_bridge prepare \ + --assets /absolute/path/to/piper-assets \ + --output /absolute/path/to/demo-scene --duration 20 + +python -m dvidia_training.godot_demonstration_bridge serve \ + --assets /absolute/path/to/piper-assets \ + --scene /absolute/path/to/demo-scene \ + --output /absolute/path/to/demo-session \ + --source-kind simulation_human_input --port 8431 --max-episodes 4 + +# In another terminal: +godot --path integrations/godot-demonstration -- \ + --scene /absolute/path/to/demo-scene --port 8431 +``` + +`simulation_human_input` identifies a session where a person operates the controls. Automated actions must use `simulation_scripted_input`; smoke mode refuses any other source. The interface displays the recorder's source, acknowledged step count, recorded duration, and output directory. + +- **Record** begins the prepared episode. No recording control intervals run before it; preparation performs the native reset and settling. +- **Pause / Resume** changes recording state without advancing simulation time. A click waits for an outstanding step to finish. +- **Finish** finalizes the current episode. Reaching the configured step limit also finalizes it. +- **New episode** finalizes the current episode and resets into a fresh output directory. It is disabled after the session's episode limit. +- **Home** returns the commanded target and jaw to the scene's initial values. Drag empty space to orbit; scroll to zoom. + +The orbiting viewport is an operator view, not the policy camera. The Python recorder captures native RGB-D and action/state records. This client has no training or physical robot controls. + +MuJoCo is the only physics authority; both Godot physics servers are disabled. Each acknowledged step advances one 20 ms control interval comprising 1 ms native physics ticks. The client requests steps at a nominal 50 Hz with exactly one outstanding request. It does not issue catch-up steps after delayed responses, so elapsed wall time can exceed simulated duration. Recorder errors and disconnections remain visible and stop further commands. + +The viewer verifies the exported manifest, each declared file's size and SHA-256, compiled model identity, and simulation clocks. Meshes and transforms use the coordinate rotation `[x, y, z] → [x, z, -y]`. Body transforms are world-space and geom transforms are body-local. The line-delimited JSON protocol is `dvidia.godot-demonstration/v1`; numbered commands start at 1, while hello and heartbeat ping are unnumbered. + +## Checks and scripted smoke + +```sh +godot --headless --path integrations/godot-demonstration --check-only --script main.gd +godot --headless --path integrations/godot-demonstration --script tests/test_coordinates.gd +godot --headless --path integrations/godot-demonstration --script tests/test_recording.gd +``` + +Start a fresh recorder session with `--source-kind simulation_scripted_input`, then run: + +```sh +godot --headless --path integrations/godot-demonstration -- \ + --scene /absolute/path/to/demo-scene --port 8431 \ + --smoke-steps 100 --smoke-output /absolute/path/to/fresh-smoke.json +``` + +The prepared duration must permit the requested number of steps. Smoke sends Begin, bounded scripted XYZ/jaw steps, and Finish, including an idempotent Finish after automatic completion. It exits successfully only after the final acknowledgement. Its diagnostic receipt contains initial state, up to 256 request/state/recording snapshots, final recording metadata, model and scene digests, server compute timing (including native camera capture and disk writes), and socket/process timing. These are diagnostics, not a training dataset or a graphics benchmark. Existing output files are preserved. + +For a windowed smoke run, omit `--headless` and add `--screenshot /absolute/path/to/fresh-image.png`. The screenshot shows the operator viewport; native policy camera images are written by the Python recorder. + +DVIDIA source is MIT licensed. Prepared exports include the original pinned PiPER asset license and attribution alongside DVIDIA's license. diff --git a/integrations/godot-demonstration/main.gd b/integrations/godot-demonstration/main.gd new file mode 100644 index 0000000..905ff0f --- /dev/null +++ b/integrations/godot-demonstration/main.gd @@ -0,0 +1,949 @@ +extends Node3D +## Local renderer, never a robot physics authority. MIT, DVIDIA contributors. + +const SCHEMA := "dvidia.godot-demonstration/v1" +const MAX_FILE := 64 * 1024 * 1024 +const MAX_PACKET := 1024 * 1024 +const INK := Color("25322e") +const MUTED := Color("728079") +const GREEN := Color("4e7562") +const PAPER := Color("f8f8f2") + +var peer := StreamPeerTCP.new() +var scene_data: Dictionary = {} +var body_nodes: Array[Node3D] = [] +var mesh_cache: Dictionary = {} +var incoming := PackedByteArray() +var connected := false +var hello_sent := false +var pending := false +var pending_kind := "" +var finished := false +var errors: Array[String] = [] +var sequence := 1 +var replies := 0 +var resets := 0 +var state_time := 0.0 +var compute_total_ms := 0.0 +var last_compute_ms := 0.0 +var last_request_us := 0 +var next_step_due_us := 0 +var started_us := 0 +var connected_us := 0 +var frame_count := 0 +var target := [0.36, -0.06, 0.27] +var jaw := 0.035 +var sliders: Array[HSlider] = [] +var value_labels: Array[Label] = [] +var status_label: Label +var time_label: Label +var compute_label: Label +var pause_button: Button +var camera: Camera3D +var marker: MeshInstance3D +var camera_yaw := -0.63 +var camera_pitch := 0.42 +var camera_distance := 1.65 +var orbiting := false +var focus := Vector3(0.26, 0.12, 0.0) +var scene_dir := "" +var port := 8431 +var smoke_steps := 0 +var smoke_output := "" +var screenshot_path := "" +var initial_sim_time := 0.0 +var initial_state: Dictionary = {} +var smoke_trace: Array[Dictionary] = [] +var pending_request: Dictionary = {} +var queued_command := "" +var recording: Dictionary = {} +var record_button: Button +var finish_button: Button +var new_button: Button +var home_button: Button +var source_label: Label +var output_label: Label +var recording_label: Label +var gripper_max := 0.035 +var scene_integrity_sha256 := "" +var verified_files: Dictionary = {} +var ready_us := 0 +var completed_us := 0 +var frame_intervals_ms: Array[float] = [] +var rpc_roundtrip_ms: Array[float] = [] + + +func _ready() -> void: + PhysicsServer3D.set_active(false) + PhysicsServer2D.set_active(false) + started_us = Time.get_ticks_usec() + _parse_args() + _build_world() + _build_ui() + if not errors.is_empty(): + _fail(errors[0]) + return + if scene_dir.is_empty(): + _fail("Choose an exported scene: --scene /absolute/path --port 8431") + return + if not _load_scene(): + return + status_label.text = "Connecting to local MuJoCo…" + var result := peer.connect_to_host("127.0.0.1", port) + if result != OK: + _fail("Cannot connect to local MuJoCo (%s)." % result) + connected_us = Time.get_ticks_usec() + + +func _parse_args() -> void: + var args := OS.get_cmdline_user_args() + var index := 0 + while index < args.size(): + var flag := args[index] + if flag not in ["--scene", "--port", "--smoke-steps", "--smoke-output", "--screenshot"] or index + 1 >= args.size(): + errors.append("Unknown or incomplete argument: " + flag) + return + var value := args[index + 1] + match flag: + "--scene": scene_dir = value + "--port": port = int(value) + "--smoke-steps": smoke_steps = int(value) + "--smoke-output": smoke_output = value + "--screenshot": screenshot_path = value + index += 2 + if port < 1024 or port > 65535 or smoke_steps < 0 or smoke_steps > 100000: + errors.append("Port or smoke step count is outside bounds.") + if (not smoke_output.is_empty() and not smoke_output.is_absolute_path()) or (not screenshot_path.is_empty() and not screenshot_path.is_absolute_path()): + errors.append("Smoke output and screenshot paths must be absolute.") + if (not smoke_output.is_empty() and FileAccess.file_exists(smoke_output)) or (not screenshot_path.is_empty() and FileAccess.file_exists(screenshot_path)): + errors.append("Smoke output and screenshot paths must be fresh files.") + + +func _build_world() -> void: + var environment_node := WorldEnvironment.new() + var environment := Environment.new() + environment.background_mode = Environment.BG_COLOR + environment.background_color = Color("e9ebe2") + environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR + environment.ambient_light_color = Color("f1f5e9") + # Compatibility rendering is SDR. Keep the light budget below clipping; + # bright HDR-style lighting washes out native RGBA materials in this path. + environment.ambient_light_energy = 0.20 + environment.tonemap_mode = Environment.TONE_MAPPER_LINEAR + environment_node.environment = environment + add_child(environment_node) + var light := DirectionalLight3D.new() + light.rotation_degrees = Vector3(-52, -38, 0) + light.light_energy = 0.42 + light.shadow_enabled = true + light.directional_shadow_max_distance = 5.0 + add_child(light) + var fill := DirectionalLight3D.new() + fill.rotation_degrees = Vector3(-32, 130, 0) + fill.light_energy = 0.08 + add_child(fill) + camera = Camera3D.new() + camera.fov = 41 + camera.near = 0.005 + camera.far = 30 + add_child(camera) + camera.make_current() + _position_camera() + marker = MeshInstance3D.new() + var sphere := SphereMesh.new() + sphere.radius = 0.008 + sphere.height = 0.016 + marker.mesh = sphere + var material := StandardMaterial3D.new() + material.albedo_color = Color("73a27e") + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + marker.material_override = material + add_child(marker) + marker.position = native_vector(target) + + +func _style(background: Color, border: Color = Color.TRANSPARENT) -> StyleBoxFlat: + var box := StyleBoxFlat.new() + box.bg_color = background + box.border_color = border + box.set_border_width_all(1) + box.set_corner_radius_all(8) + box.content_margin_left = 16 + box.content_margin_right = 16 + box.content_margin_top = 11 + box.content_margin_bottom = 11 + return box + + +func _label(text: String, size: int = 16, color: Color = INK) -> Label: + var label := Label.new() + label.text = text + label.add_theme_font_size_override("font_size", size) + label.add_theme_color_override("font_color", color) + return label + + +func _button(text: String, action: Callable) -> Button: + var button := Button.new() + button.text = text + button.custom_minimum_size.y = 42 + button.add_theme_font_size_override("font_size", 15) + button.add_theme_color_override("font_color", INK) + button.add_theme_color_override("font_hover_color", INK) + button.add_theme_color_override("font_disabled_color", MUTED) + button.add_theme_stylebox_override("disabled", _style(Color("eef0e8"), Color("e2e6dc"))) + button.add_theme_stylebox_override("normal", _style(Color("eff1e9"), Color("d8ded1"))) + button.add_theme_stylebox_override("hover", _style(Color("e1e8dc"))) + button.add_theme_stylebox_override("pressed", _style(Color("d3dfcd"))) + button.pressed.connect(action) + return button + + +func _build_ui() -> void: + var layer := CanvasLayer.new() + add_child(layer) + var root := Control.new() + root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + root.mouse_filter = Control.MOUSE_FILTER_IGNORE + layer.add_child(root) + var header := PanelContainer.new() + header.set_anchors_and_offsets_preset(Control.PRESET_TOP_WIDE) + header.offset_bottom = 72 + header.add_theme_stylebox_override("panel", _style(PAPER, Color("d9ded2"))) + root.add_child(header) + var top := HBoxContainer.new() + top.add_theme_constant_override("separation", 18) + header.add_child(top) + top.add_child(_label("DVIDIA", 23)) + top.add_child(_label("/ Demonstration studio", 16, MUTED)) + var spacer := Control.new() + spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL + top.add_child(spacer) + top.add_child(_label("MuJoCo SIMULATION · LOCAL", 13, GREEN)) + var panel := PanelContainer.new() + panel.set_anchors_and_offsets_preset(Control.PRESET_RIGHT_WIDE) + panel.offset_left = -370 + panel.offset_right = -22 + panel.offset_top = 94 + panel.offset_bottom = -22 + panel.add_theme_stylebox_override("panel", _style(PAPER, Color("d9ded2"))) + root.add_child(panel) + var scroll := ScrollContainer.new() + scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED + panel.add_child(scroll) + var column := VBoxContainer.new() + column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_theme_constant_override("separation", 8) + scroll.add_child(column) + column.add_child(_label("Block demonstrations", 22)) + column.add_child(_label("Position + jaw · Fixed tool orientation", 13, MUTED)) + status_label = _label("Opening scene…", 14, GREEN) + status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + status_label.custom_minimum_size = Vector2(294, 32) + column.add_child(status_label) + source_label = _label("Source —", 13, MUTED) + column.add_child(source_label) + for index in range(4): + var row := HBoxContainer.new() + var names := ["Target X", "Target Y", "Target Z", "Jaw opening"] + var caption := _label(names[index], 14) + caption.size_flags_horizontal = Control.SIZE_EXPAND_FILL + row.add_child(caption) + var value_label := _label("", 14, MUTED) + row.add_child(value_label) + value_labels.append(value_label) + column.add_child(row) + var slider := HSlider.new() + slider.custom_minimum_size.y = 20 + slider.step = 0.001 + slider.editable = false + slider.value_changed.connect(_slider_changed.bind(index)) + sliders.append(slider) + column.add_child(slider) + var buttons := HBoxContainer.new() + buttons.add_theme_constant_override("separation", 8) + record_button = _button("Record", _record) + record_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + buttons.add_child(record_button) + pause_button = _button("Pause", _toggle_pause) + pause_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + buttons.add_child(pause_button) + finish_button = _button("Finish", _finish_episode) + finish_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + buttons.add_child(finish_button) + column.add_child(buttons) + var secondary := HBoxContainer.new() + secondary.add_theme_constant_override("separation", 8) + home_button = _button("Home", _home) + secondary.add_child(home_button) + new_button = _button("New episode", _reset) + new_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + secondary.add_child(new_button) + column.add_child(secondary) + recording_label = _label("Recorded 0 steps", 15) + time_label = _label("Recorded duration 0.00 s", 14) + compute_label = _label("Server step — ms", 12, MUTED) + column.add_child(recording_label) + column.add_child(time_label) + column.add_child(compute_label) + column.add_child(_label("EPISODE DIRECTORY", 11, MUTED)) + output_label = _label("Waiting for the recorder…", 12, MUTED) + output_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + output_label.custom_minimum_size.x = 294 + column.add_child(output_label) + var note := _label("Simulation only · Operator viewport\nPolicy images come from the native camera.", 12, MUTED) + note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + column.add_child(note) + var hints := _label("Drag empty space to orbit · Scroll to zoom\nGreen point: commanded target · Z is native height", 14, MUTED) + hints.set_anchors_and_offsets_preset(Control.PRESET_BOTTOM_LEFT) + hints.offset_left = 28 + hints.offset_top = -63 + hints.offset_bottom = -18 + root.add_child(hints) + _refresh_controls() + + +static func native_vector(value: Array) -> Vector3: + return Vector3(float(value[0]), float(value[2]), -float(value[1])) + + +static func native_basis(value: Array) -> Basis: + # A proper coordinate rotation C: [x,y,z] -> [x,z,-y]. Both local + # mesh coordinates and every transform are mapped, so R' = C R C^-1. + return Basis(Quaternion(float(value[1]), float(value[3]), -float(value[2]), float(value[0]))) + + +func _valid_vector(value: Variant, length: int) -> bool: + if not value is Array or value.size() != length: + return false + for number in value: + if not (number is int or number is float) or not is_finite(float(number)): + return false + return true + + +func _valid_quaternion(value: Variant) -> bool: + if not _valid_vector(value, 4): + return false + var norm_squared := 0.0 + for number in value: + norm_squared += float(number) * float(number) + return absf(norm_squared - 1.0) < 0.00001 + + +func _load_scene() -> bool: + if not _verify_scene_files(): + return false + var decoded: Variant = JSON.parse_string(verified_files["scene.json"].get_string_from_utf8()) + if not decoded is Dictionary or decoded.get("schema") != "dvidia.asset-preview-model/v1" or not decoded.has("bridge") or not decoded.has("bodies") or not decoded.has("meshes") or not decoded.has("geoms"): + _fail("Scene metadata is incomplete.") + return false + scene_data = decoded + var bridge: Dictionary = scene_data.bridge + if bridge.get("schema") != SCHEMA or not str(bridge.get("model_sha256", "")).is_valid_hex_number(false) or str(bridge.get("model_sha256", "")).length() != 64: + _fail("Scene bridge identity is invalid.") + return false + if float(bridge.get("physics_dt", -1)) != 0.001 or float(bridge.get("control_dt", -1)) != 0.02 or scene_data.get("source", {}).get("compiled_model_sha256") != bridge.model_sha256: + _fail("Scene model source or pinned simulation clocks differ.") + return false + var info: Dictionary = scene_data.get("geometry_binary", {}) + if info.get("href") != "geometry.bin" or info.get("compression") != "none" or int(info.get("bytes", -1)) < 0 or int(info.get("bytes", MAX_FILE + 1)) > MAX_FILE: + _fail("Scene geometry framing is invalid.") + return false + var bytes: PackedByteArray = verified_files["geometry.bin"] + if bytes.size() != int(info.bytes): + _fail("Geometry file is missing or has changed size.") + return false + var hash := HashingContext.new() + hash.start(HashingContext.HASH_SHA256) + hash.update(bytes) + if hash.finish().hex_encode() != info.get("sha256"): + _fail("Geometry checksum does not match the exported scene.") + return false + if scene_data.bodies.size() < 1 or scene_data.bodies.size() > 128 or scene_data.meshes.size() > 128 or scene_data.geoms.size() > 512: + _fail("Scene inventory exceeds bounds.") + return false + for body in scene_data.bodies: + if int(body.id) != body_nodes.size(): + _fail("Body IDs must be contiguous native IDs.") + return false + var node := Node3D.new() + node.name = str(body.name) + add_child(node) + body_nodes.append(node) + for record in scene_data.meshes: + var mesh := _native_mesh(record, bytes) + if mesh == null: + return false + mesh_cache[int(record.id)] = mesh + for geom in scene_data.geoms: + if not _add_geom(geom): + return false + if not _valid_vector(bridge.get("home_target"), 3) or not bridge.get("bounds") is Array or bridge.bounds.size() != 3: + _fail("Target declaration is invalid.") + return false + target = bridge.home_target.duplicate() + jaw = float(bridge.gripper_initial) + gripper_max = float(bridge.get("gripper_max", 0.035)) + for index in range(4): + if index < 3: + sliders[index].min_value = float(bridge.bounds[index][0]) + sliders[index].max_value = float(bridge.bounds[index][1]) + else: + sliders[index].min_value = 0 + sliders[index].max_value = gripper_max + _set_slider(index, float(target[index]) if index < 3 else jaw) + verified_files.clear() + return true + + +func _verify_scene_files() -> bool: + var seal_file := FileAccess.open(scene_dir.path_join("scene-integrity.json"), FileAccess.READ) + if seal_file == null or seal_file.get_length() > 65536: + _fail("Cannot read a bounded scene-integrity.json.") + return false + var seal_bytes := seal_file.get_buffer(seal_file.get_length()) + scene_integrity_sha256 = _sha256(seal_bytes) + var seal: Variant = JSON.parse_string(seal_bytes.get_string_from_utf8()) + if not seal is Dictionary or seal.get("schema") != "dvidia.godot-scene-integrity/v1" or not seal.get("files") is Dictionary: + _fail("Scene integrity manifest is invalid.") + return false + var expected := ["scene.json", "geometry.bin", "LICENSE", "licenses/agilex_piper/LICENSE", "licenses/agilex_piper/README.md", "README.md"] + if seal.files.size() != expected.size(): + _fail("Scene integrity inventory differs from the pinned PiPER export.") + return false + var total := 0 + for name in expected: + var record: Variant = seal.files.get(name) + if not record is Dictionary or not record.get("bytes") is float and not record.get("bytes") is int: + _fail("Scene integrity file record is invalid.") + return false + var size := int(record.bytes) + var limit := MAX_FILE if name == "geometry.bin" else (12 * 1024 * 1024 if name == "scene.json" else 65536) + var file := FileAccess.open(scene_dir.path_join(name), FileAccess.READ) + if size < 0 or size > limit or file == null or file.get_length() != size: + _fail("Missing or size-mismatched scene artifact: " + name) + return false + var payload := file.get_buffer(size) + if payload.size() != size or _sha256(payload) != record.get("sha256"): + _fail("Scene artifact checksum mismatch: " + name) + return false + total += size + if total > 96 * 1024 * 1024: + _fail("Scene artifact total exceeds bounds.") + return false + if name in ["scene.json", "geometry.bin"]: + verified_files[name] = payload + var metadata: Variant = JSON.parse_string(verified_files["scene.json"].get_string_from_utf8()) + if not metadata is Dictionary or not metadata.get("bridge") is Dictionary or metadata.bridge.get("model_sha256") != seal.get("model_sha256"): + _fail("Scene model identity differs from its integrity manifest.") + return false + return true + + +func _sha256(bytes: PackedByteArray) -> String: + var hash := HashingContext.new() + hash.start(HashingContext.HASH_SHA256) + hash.update(bytes) + return hash.finish().hex_encode() + + +func _native_mesh(record: Dictionary, bytes: PackedByteArray) -> ArrayMesh: + var count := int(record.vertex_count) + var indices := int(record.index_count) + var vertex_offset := int(record.position_byte_offset) + var index_offset := int(record.index_byte_offset) + if count < 1 or indices < 3 or indices % 3 != 0 or vertex_offset < 0 or index_offset < 0 or vertex_offset + count * 12 > bytes.size() or index_offset + indices * 4 > bytes.size(): + _fail("Native mesh byte range is invalid.") + return null + var points := PackedVector3Array() + points.resize(count) + for index in range(count): + var offset := vertex_offset + index * 12 + var value := [bytes.decode_float(offset), bytes.decode_float(offset + 4), bytes.decode_float(offset + 8)] + if not _valid_vector(value, 3): + _fail("Native mesh coordinate is nonfinite.") + return null + points[index] = native_vector(value) + var triangle_indices := PackedInt32Array() + triangle_indices.resize(indices) + for index in range(0, indices, 3): + # MuJoCo triangle winding is counterclockwise; Godot fronts are clockwise. + for corner in range(3): + var source_corner: int = [0, 2, 1][corner] + var vertex := int(bytes.decode_u32(index_offset + (index + source_corner) * 4)) + if vertex < 0 or vertex >= count: + _fail("Native triangle index is outside the mesh.") + return null + triangle_indices[index + corner] = vertex + var arrays := [] + arrays.resize(Mesh.ARRAY_MAX) + arrays[Mesh.ARRAY_VERTEX] = points + arrays[Mesh.ARRAY_INDEX] = triangle_indices + var mesh := ArrayMesh.new() + mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays) + var surface := SurfaceTool.new() + surface.create_from(mesh, 0) + surface.generate_normals() + return surface.commit() + + +func _add_geom(geom: Dictionary) -> bool: + if not _valid_vector(geom.get("position"), 3) or not _valid_quaternion(geom.get("quaternion_wxyz")) or not _valid_vector(geom.get("size"), 3) or not _valid_vector(geom.get("rgba"), 4): + _fail("Native geometry transform is invalid.") + return false + var body_id := int(geom.get("body_id", -1)) + if body_id < 0 or body_id >= body_nodes.size(): + _fail("Geometry references an unknown body.") + return false + if float(geom.rgba[3]) <= 0.0 or int(geom.group) > 2: + return true + var size: Array = geom.size + var mesh: Mesh + var scale_value := Vector3.ONE + match geom.type: + "mesh": mesh = mesh_cache.get(int(geom.mesh_id)) + "box": + var box := BoxMesh.new() + box.size = Vector3(2 * size[0], 2 * size[2], 2 * size[1]) + mesh = box + "sphere", "ellipsoid": + var sphere := SphereMesh.new() + sphere.radius = 1.0 if geom.type == "ellipsoid" else float(size[0]) + sphere.height = 2 * sphere.radius + if geom.type == "ellipsoid": + scale_value = Vector3(size[0], size[2], size[1]) + mesh = sphere + "capsule": + var capsule := CapsuleMesh.new() + capsule.radius = size[0] + capsule.height = 2 * (size[0] + size[1]) + mesh = capsule + "cylinder": + var cylinder := CylinderMesh.new() + cylinder.top_radius = size[0] + cylinder.bottom_radius = size[0] + cylinder.height = 2 * size[1] + mesh = cylinder + "plane": + var plane := PlaneMesh.new() + plane.size = Vector2(maxf(1, 2 * size[0]), maxf(1, 2 * size[1])) + mesh = plane + if mesh == null: + _fail("Unsupported or missing native geometry: " + str(geom.type)) + return false + var instance := MeshInstance3D.new() + instance.name = str(geom.name) + instance.mesh = mesh + var rgba: Array = geom.rgba + var material := StandardMaterial3D.new() + material.albedo_color = Color(rgba[0], rgba[1], rgba[2], rgba[3]).linear_to_srgb() + material.roughness = 0.7 + material.metallic = 0.12 if geom.type == "mesh" else 0.0 + material.cull_mode = BaseMaterial3D.CULL_DISABLED + if float(rgba[3]) < 1: + material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA + instance.material_override = material + instance.transform = Transform3D(native_basis(geom.quaternion_wxyz).scaled_local(scale_value), native_vector(geom.position)) + body_nodes[body_id].add_child(instance) + return true + + +func _process(delta: float) -> void: + frame_count += 1 + if finished or not errors.is_empty(): + return + if connected and frame_intervals_ms.size() < 10000: + frame_intervals_ms.append(delta * 1000.0) + peer.poll() + var status := peer.get_status() + if status == StreamPeerTCP.STATUS_CONNECTED: + if not hello_sent: + hello_sent = true + _send({"type": "hello", "schema": SCHEMA, "model_sha256": scene_data.bridge.model_sha256}) + var available := peer.get_available_bytes() + if available > 0: + if incoming.size() + available > MAX_PACKET: + _fail("Bridge packet exceeds bounds.") + return + var received := peer.get_data(available) + if received[0] != OK: + _fail("Local bridge read failed.") + return + incoming.append_array(received[1]) + while incoming.find(10) >= 0: + var end := incoming.find(10) + var message: Variant = JSON.parse_string(incoming.slice(0, end).get_string_from_utf8()) + incoming = incoming.slice(end + 1) + if not message is Dictionary: + _fail("Bridge returned invalid JSON.") + return + _receive(message) + if finished or not errors.is_empty(): + return + if connected and not pending: + if not queued_command.is_empty(): + var command := queued_command + queued_command = "" + _send({"type": command, "sequence": sequence}) + elif recording.get("status") == "recording" and Time.get_ticks_usec() >= next_step_due_us: + if smoke_steps > 0: + _smoke_target() + # One pending request. Late replies discard elapsed deadlines; + # neither rendering nor a catch-up loop determines physics ticks. + next_step_due_us += 20000 + _send({"type": "step", "sequence": sequence, "target": target, "jaw": jaw}) + elif Time.get_ticks_usec() - last_request_us >= 5000000: + _send({"type": "ping"}) + elif status == StreamPeerTCP.STATUS_ERROR or hello_sent: + _fail("MuJoCo disconnected. Recording stopped; check the episode directory.") + return + if (pending and Time.get_ticks_usec() - last_request_us > 10000000) or (not hello_sent and Time.get_ticks_usec() - connected_us > 10000000): + _fail("Local MuJoCo response timed out. Recording stopped.") + + +func _send(message: Dictionary) -> void: + if pending: + _fail("Refusing a second outstanding recorder request.") + return + pending_kind = str(message.type) + pending_request = message.duplicate(true) + if peer.put_data((JSON.stringify(message) + "\n").to_utf8_buffer()) != OK: + _fail("Local bridge write failed.") + return + pending = true + last_request_us = Time.get_ticks_usec() + _refresh_controls() + + +static func valid_recording(value: Variant) -> bool: + if not value is Dictionary or value.get("status") not in ["ready", "recording", "paused", "finished"]: + return false + for key in ["steps", "max_steps"]: + var number: Variant = value.get(key) + if not (number is int or number is float) or not is_finite(float(number)) or float(number) != floor(float(number)): + return false + if value.steps < 0 or value.max_steps < 1 or value.steps > value.max_steps: + return false + return value.get("source_kind") in ["simulation_human_input", "simulation_scripted_input"] and value.get("output_directory") is String and value.output_directory.is_absolute_path() + + +static func recording_transition_error(previous: Dictionary, current: Dictionary, command: String) -> String: + if not valid_recording(current): + return "Recorder state framing is invalid." + if command in ["hello", "reset"]: + if current.status != "ready" or int(current.steps) != 0: + return "A new episode must be ready with zero recorded steps." + if command == "reset" and current.output_directory == previous.get("output_directory"): + return "New episode did not receive a fresh output directory." + return "" + if previous.is_empty() or current.output_directory != previous.get("output_directory") or current.source_kind != previous.get("source_kind") or current.max_steps != previous.get("max_steps"): + return "Recorder episode identity changed unexpectedly." + var expected_steps := int(previous.steps) + (1 if command == "step" else 0) + if int(current.steps) != expected_steps: + return "Recorded step count differs from acknowledged actions." + var expected_status: String = {"begin": "recording", "resume": "recording", "pause": "paused", "finish": "finished", "step": "recording"}.get(command, "") + if command == "step" and int(current.steps) == int(current.max_steps) and current.status == "finished": + return "" + if current.status != expected_status: + return "Recorder status differs from the acknowledged command." + return "" + + +func _receive(message: Dictionary) -> void: + var received_us := Time.get_ticks_usec() + if message.get("type") == "error": + _fail("Recorder: " + str(message.get("message", "request rejected"))) + return + if pending and pending_kind == "ping" and message.get("type") == "pong" and message.get("model_sha256") == scene_data.bridge.model_sha256 and int(message.get("expected_sequence", -1)) == sequence: + pending = false + pending_kind = "" + _refresh_controls() + return + if not pending or message.get("model_sha256") != scene_data.bridge.model_sha256: + _fail("Unsolicited response or model identity mismatch.") + return + var command := pending_kind + if command == "hello": + if message.get("type") != "ready" or message.get("schema") != SCHEMA or float(message.get("physics_dt", -1)) != float(scene_data.bridge.physics_dt) or float(message.get("control_dt", -1)) != float(scene_data.bridge.control_dt): + _fail("Local MuJoCo handshake differs from the scene.") + return + else: + if message.get("type") != "state" or int(message.get("sequence", -1)) != sequence: + _fail("Bridge response sequence mismatch.") + return + var current: Variant = message.get("recording") + if not current is Dictionary: + _fail("Recorder state is missing.") + return + var transition_error := recording_transition_error(recording, current, command) + if not transition_error.is_empty(): + _fail(transition_error) + return + if smoke_steps > 0 and current.source_kind != "simulation_scripted_input": + _fail("Smoke capture requires a server configured with source_kind simulation_scripted_input.") + return + if command == "hello" and smoke_steps > int(current.max_steps): + _fail("Requested smoke steps exceed the episode limit.") + return + var state: Variant = message.get("state") + if not state is Dictionary or not state.get("body_positions") is Array or not state.get("body_quaternions") is Array or state.body_positions.size() != body_nodes.size() or state.body_quaternions.size() != body_nodes.size(): + _fail("Native body state framing mismatch.") + return + for index in range(body_nodes.size()): + if not _valid_vector(state.body_positions[index], 3) or not _valid_quaternion(state.body_quaternions[index]): + _fail("Native body pose is nonfinite.") + return + var new_time := float(state.get("time", -1)) + if not is_finite(new_time) or new_time < 0: + _fail("Native simulation clock is invalid.") + return + if command in ["hello", "reset"]: + if absf(new_time) > 0.000001: + _fail("New episode simulation time must start at zero.") + return + else: + var expected_delta := float(scene_data.bridge.control_dt) if command == "step" else 0.0 + if absf(new_time - state_time - expected_delta) > 0.000001: + _fail("Native clock changed outside the acknowledged control interval.") + return + var compute_seconds := float(message.get("step_compute_wall_seconds", 0)) + if not is_finite(compute_seconds) or compute_seconds < 0: + _fail("Native compute timing is invalid.") + return + for index in range(body_nodes.size()): + body_nodes[index].transform = Transform3D(native_basis(state.body_quaternions[index]), native_vector(state.body_positions[index])) + state_time = new_time + recording = current.duplicate(true) + if command == "hello": + connected = true + ready_us = received_us + initial_sim_time = new_time + initial_state = state.duplicate(true) + if smoke_steps > 0: + queued_command = "begin" + else: + sequence += 1 + if command == "reset": + resets += 1 + _home() + elif command == "step": + replies += 1 + last_compute_ms = compute_seconds * 1000.0 + compute_total_ms += last_compute_ms + if rpc_roundtrip_ms.size() < 10000: + rpc_roundtrip_ms.append((received_us - last_request_us) / 1000.0) + if command in ["hello", "reset", "begin", "resume"] or (command == "step" and received_us > next_step_due_us): + next_step_due_us = received_us + 20000 + if smoke_steps > 0 and command != "hello" and smoke_trace.size() < 256: + smoke_trace.append({"request": pending_request.duplicate(true), "state": state.duplicate(true), "recording": recording.duplicate(true), "step_compute_wall_seconds": compute_seconds, "rpc_roundtrip_ms": (received_us - last_request_us) / 1000.0}) + pending = false + pending_kind = "" + if recording.status == "finished" and queued_command != "reset": + queued_command = "" + _refresh_controls() + if smoke_steps > 0 and replies >= smoke_steps: + if command == "finish": + _finish_smoke() + else: + _queue_command("finish") + + +func _refresh_controls() -> void: + if record_button == null: + return + var available := connected and errors.is_empty() and not finished + var busy := not queued_command.is_empty() or (pending and pending_kind not in ["step", "ping"]) + var status: String = str(recording.get("status", "")) + record_button.disabled = not available or busy or status != "ready" + pause_button.disabled = not available or busy or status not in ["recording", "paused"] + pause_button.text = "Resume" if status == "paused" else "Pause" + finish_button.disabled = not available or busy or status not in ["recording", "paused"] + var budget_exhausted := recording.has("episode") and recording.has("max_episodes") and int(recording.episode) >= int(recording.max_episodes) + new_button.disabled = not available or busy or budget_exhausted + new_button.tooltip_text = "Episode limit reached. Start a new recorder session." if budget_exhausted else "Finish this episode and prepare a fresh directory." + home_button.disabled = not available or busy or status == "finished" + for slider in sliders: + slider.editable = available and not busy and status != "finished" and smoke_steps == 0 + if not available or recording.is_empty(): + return + var descriptions := {"ready": "Ready · Press Record to begin", "recording": "Recording · MuJoCo connected", "paused": "Paused · No simulation steps", "finished": "Finished · Episode saved"} + status_label.text = descriptions.get(status, status) + if not queued_command.is_empty(): + status_label.text = "Waiting to %s after the current request…" % queued_command + elif busy: + status_label.text = "Recorder: %s…" % pending_kind + var source_name: String = {"simulation_human_input": "Human operator", "simulation_scripted_input": "Scripted smoke"}.get(recording.source_kind, str(recording.source_kind)) + source_label.text = "Source %s" % source_name + source_label.tooltip_text = str(recording.source_kind) + recording_label.text = "Recorded %d / %d steps" % [int(recording.steps), int(recording.max_steps)] + time_label.text = "Recorded duration %.2f s" % (int(recording.steps) * 0.02) + compute_label.text = "Server step %.2f ms · 20 ms simulated" % last_compute_ms + var directory := str(recording.output_directory) + output_label.text = directory.get_base_dir().get_file() + " / " + directory.get_file() + output_label.tooltip_text = str(recording.output_directory) + + +func _queue_command(command: String) -> void: + if not connected or not errors.is_empty() or not queued_command.is_empty(): + return + queued_command = command + _refresh_controls() + + +func _slider_changed(value: float, index: int) -> void: + if index < 3: + target[index] = value + else: + jaw = value + value_labels[index].text = "%.0f mm" % (value * 1000) if index < 3 else "%.0f%% open" % (value / gripper_max * 100) + marker.position = native_vector(target) + + +func _set_slider(index: int, value: float) -> void: + sliders[index].set_value_no_signal(value) + _slider_changed(sliders[index].value, index) + + +func _record() -> void: + if recording.get("status") == "ready": + _queue_command("begin") + + +func _toggle_pause() -> void: + if recording.get("status") in ["recording", "paused"]: + _queue_command("resume" if recording.status == "paused" else "pause") + + +func _finish_episode() -> void: + if recording.get("status") in ["recording", "paused"]: + _queue_command("finish") + + +func _home() -> void: + if not connected: + return + for index in range(3): + _set_slider(index, scene_data.bridge.home_target[index]) + _set_slider(3, scene_data.bridge.gripper_initial) + + +func _reset() -> void: + _queue_command("reset") + + +func _smoke_target() -> void: + # Bounded, deterministic movement and jaw changes verify more than a static + # connection. The trace preserves the exact transmitted action for replay. + var fraction := float(replies) / float(smoke_steps) + var offset := [0.0, 0.0, 0.0] + var opening := 0.035 + if fraction >= 0.333 and fraction < 0.666: + offset = [0.02, 0.025, -0.025] + opening = 0.025 + elif fraction >= 0.666: + offset = [0.035, -0.015, 0.01] + opening = 0.01 + for index in range(3): + _set_slider(index, float(scene_data.bridge.home_target[index]) + float(offset[index])) + _set_slider(3, opening) + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseButton: + if event.button_index == MOUSE_BUTTON_LEFT: + orbiting = event.pressed + elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_UP: + camera_distance = maxf(0.45, camera_distance * 0.9) + elif event.pressed and event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + camera_distance = minf(4.5, camera_distance * 1.1) + _position_camera() + elif event is InputEventMouseMotion and orbiting: + camera_yaw -= event.relative.x * 0.006 + camera_pitch = clampf(camera_pitch + event.relative.y * 0.006, 0.08, 1.35) + _position_camera() + + +func _position_camera() -> void: + if camera == null: + return + camera.position = focus + camera_distance * Vector3(cos(camera_pitch) * sin(camera_yaw), sin(camera_pitch), cos(camera_pitch) * cos(camera_yaw)) + camera.look_at(focus) + # Shift the scene away from the overlaid controls without moving its geometry. + camera.h_offset = 0.19 + + +func _fail(message: String) -> void: + if errors.is_empty() or errors[-1] != message: + errors.append(message) + connected = false + queued_command = "" + if status_label != null: + status_label.text = message + status_label.add_theme_color_override("font_color", Color("a14d43")) + for slider in sliders: + slider.editable = false + _refresh_controls() + peer.disconnect_from_host() + push_error(message) + if smoke_steps > 0 or not smoke_output.is_empty(): + _finish_smoke() + + +func _finish_smoke() -> void: + if finished: + return + finished = true + completed_us = Time.get_ticks_usec() + peer.disconnect_from_host() + if not screenshot_path.is_empty() and DisplayServer.get_name() != "headless": + await RenderingServer.frame_post_draw + if FileAccess.file_exists(screenshot_path): + errors.append("Existing screenshot was preserved.") + else: + var saved := get_viewport().get_texture().get_image().save_png(screenshot_path) + if saved != OK: + errors.append("Screenshot could not be written.") + var summary := { + "schema": "dvidia.godot-demonstration-smoke/v1", "ok": errors.is_empty(), + "client_script_sha256": FileAccess.get_sha256("res://main.gd"), + "project_sha256": FileAccess.get_sha256("res://project.godot"), + "processor": OS.get_processor_name(), "logical_processors": OS.get_processor_count(), + "expected_model_sha256": scene_data.get("bridge", {}).get("model_sha256", ""), + "scene_integrity_sha256": scene_integrity_sha256, + "step_responses": replies, "reset_responses": resets, "requested_steps": smoke_steps, + "initial_sim_time_seconds": initial_sim_time, "applied_sim_time_seconds": state_time, + "initial_state": initial_state, "recording": recording, "trace": smoke_trace, "trace_limit": 256, + "wall_seconds": (Time.get_ticks_usec() - started_us) / 1000000.0, + "interactive_wall_seconds": (completed_us - ready_us) / 1000000.0 if ready_us > 0 else 0.0, + "process_frame_interval_ms": _distribution(frame_intervals_ms), + "rpc_roundtrip_ms": _distribution(rpc_roundtrip_ms), + "timing_scope": "Startup-inclusive wall time; ready-to-completion interactive wall; Godot process delta intervals (not GPU render time); socket request-to-parsed-response latency; server compute includes controller, native camera capture and disk writes; excludes IPC and Godot rendering. Samples capped at 10000 per series.", + "server_compute_total_ms": compute_total_ms, "render_process_frames": frame_count, + "physics_authority": "MuJoCo only", "godot": Engine.get_version_info().string, + "errors": errors, + } + if not smoke_output.is_empty(): + if FileAccess.file_exists(smoke_output): + push_error("Existing smoke output was preserved.") + get_tree().quit(1) + return + var output := FileAccess.open(smoke_output, FileAccess.WRITE) + if output == null: + push_error("Cannot write smoke output.") + get_tree().quit(1) + return + output.store_string(JSON.stringify(summary, " ") + "\n") + var compact := summary.duplicate() + compact.erase("initial_state") + compact.erase("trace") + print(JSON.stringify(compact)) + get_tree().quit(0 if errors.is_empty() else 1) + + +func _distribution(values: Array[float]) -> Dictionary: + if values.is_empty(): + return {"samples": 0, "p50": null, "p95": null} + var ordered := values.duplicate() + ordered.sort() + return {"samples": values.size(), "p50": ordered[int(floor((ordered.size() - 1) * 0.5))], "p95": ordered[int(ceil((ordered.size() - 1) * 0.95))]} diff --git a/integrations/godot-demonstration/main.tscn b/integrations/godot-demonstration/main.tscn new file mode 100644 index 0000000..7b3efcb --- /dev/null +++ b/integrations/godot-demonstration/main.tscn @@ -0,0 +1,6 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://main.gd" id="1"] + +[node name="DvidiaDemonstration" type="Node3D"] +script = ExtResource("1") diff --git a/integrations/godot-demonstration/project.godot b/integrations/godot-demonstration/project.godot new file mode 100644 index 0000000..8e0c111 --- /dev/null +++ b/integrations/godot-demonstration/project.godot @@ -0,0 +1,23 @@ +; DVIDIA Godot demonstration recorder. Rendering and interface only; MuJoCo owns physics. +config_version=5 + +[application] +config/name="DVIDIA · Demonstration studio" +run/main_scene="res://main.tscn" + +[display] +window/size/viewport_width=1280 +window/size/viewport_height=800 +window/size/window_width_override=1280 +window/size/window_height_override=800 +window/stretch/mode="canvas_items" + +[rendering] +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +anti_aliasing/quality/msaa_3d=2 +textures/default_filters/use_nearest_mipmap_filter=false +environment/defaults/default_clear_color=Color(0.94, 0.94, 0.90, 1) + +[physics] +common/physics_ticks_per_second=50 diff --git a/integrations/godot-demonstration/tests/test_coordinates.gd b/integrations/godot-demonstration/tests/test_coordinates.gd new file mode 100644 index 0000000..44aa191 --- /dev/null +++ b/integrations/godot-demonstration/tests/test_coordinates.gd @@ -0,0 +1,31 @@ +extends SceneTree +## Run: godot --headless --path integrations/godot-demonstration --script tests/test_coordinates.gd + +const Workstation = preload("res://main.gd") + + +func _init() -> void: + var checks := 0 + for pair in [ + [[1, 0, 0], Vector3.RIGHT], [[0, 1, 0], Vector3(0, 0, -1)], + [[0, 0, 1], Vector3.UP], [[0.3, -0.2, 0.7], Vector3(0.3, 0.7, 0.2)], + ]: + assert(Workstation.native_vector(pair[0]).is_equal_approx(pair[1])) + checks += 1 + var converted := Workstation.native_basis([cos(PI / 4), 0, 0, sin(PI / 4)]) + assert((converted * Vector3.RIGHT).is_equal_approx(Vector3(0, 0, -1))) + assert(is_equal_approx(converted.determinant(), 1.0)) + checks += 2 + # body(R * local + p) must equal C*body(R*local+p), with no double axis + # conversion and no parent-body transformation of an already-world pose. + var native_rotation := Basis(Quaternion(Vector3(0.3, 0.7, -0.2).normalized(), 0.81)) + var q := native_rotation.get_rotation_quaternion() + var mapped_rotation := Workstation.native_basis([q.w, q.x, q.y, q.z]) + var local := Vector3(0.12, -0.03, 0.08) + var position := Vector3(0.26, -0.14, 0.22) + var native_result := native_rotation * local + position + var godot_result := mapped_rotation * Workstation.native_vector([local.x, local.y, local.z]) + Workstation.native_vector([position.x, position.y, position.z]) + assert(godot_result.distance_to(Workstation.native_vector([native_result.x, native_result.y, native_result.z])) < 0.000001) + checks += 1 + print(JSON.stringify({"schema": "dvidia.godot-demonstration-coordinates-test/v1", "passed": checks})) + quit(0) diff --git a/integrations/godot-demonstration/tests/test_recording.gd b/integrations/godot-demonstration/tests/test_recording.gd new file mode 100644 index 0000000..994611f --- /dev/null +++ b/integrations/godot-demonstration/tests/test_recording.gd @@ -0,0 +1,60 @@ +extends SceneTree +## Recorder contract assertions without a socket or an imported robot scene. + +const Demonstration = preload("res://main.gd") + + +func episode(status: String = "ready", steps: int = 0) -> Dictionary: + return {"status": status, "steps": steps, "max_steps": 3, "source_kind": "simulation_scripted_input", "output_directory": "/tmp/dvidia-test/episode-001"} + + +func _init() -> void: + var checks := 0 + assert(Demonstration.SCHEMA == "dvidia.godot-demonstration/v1") + assert(Demonstration.valid_recording(episode())) + checks += 2 + for invalid in [null, {}, {"status": "training"}, episode("ready", -1), episode("recording", 4)]: + assert(not Demonstration.valid_recording(invalid)) + checks += 1 + for replacement in [{"steps": 0.5}, {"steps": NAN}, {"max_steps": 0}, {"source_kind": "human"}, {"output_directory": "relative/episode"}]: + var invalid := episode() + invalid.merge(replacement, true) + assert(not Demonstration.valid_recording(invalid)) + checks += 1 + var ready := episode() + var recording := episode("recording") + var first := episode("recording", 1) + var paused := episode("paused", 1) + var finished := episode("finished", 1) + for transition in [[{}, ready, "hello"], [ready, recording, "begin"], [recording, first, "step"], [first, paused, "pause"], [paused, first, "resume"], [paused, finished, "finish"], [finished, finished, "finish"], [episode("recording", 2), episode("finished", 3), "step"]]: + assert(Demonstration.recording_transition_error(transition[0], transition[1], transition[2]).is_empty()) + checks += 1 + for transition in [[ready, first, "begin"], [first, episode("paused", 2), "pause"], [first, finished, "step"], [first, ready, "reset"]]: + assert(not Demonstration.recording_transition_error(transition[0], transition[1], transition[2]).is_empty()) + checks += 1 + var new_episode := episode() + new_episode.output_directory = "/tmp/dvidia-test/episode-002" + assert(Demonstration.recording_transition_error(finished, new_episode, "reset").is_empty()) + assert(not Demonstration.recording_transition_error(first, new_episode, "step").is_empty()) + checks += 2 + # Programmatic Home/smoke commands must use the slider's clamped value. + var client := Demonstration.new() + var slider := HSlider.new() + slider.min_value = 0.1 + slider.max_value = 0.5 + slider.step = 0.001 + client.sliders.append(slider) + client.value_labels.append(Label.new()) + client.marker = MeshInstance3D.new() + client._set_slider(0, 0.55) + assert(is_equal_approx(client.target[0], 0.5)) + client._set_slider(0, -0.01) + assert(is_equal_approx(client.target[0], 0.1)) + assert(is_equal_approx(client.marker.position.x, 0.1)) + checks += 3 + slider.free() + client.value_labels[0].free() + client.marker.free() + client.free() + print(JSON.stringify({"schema": "dvidia.godot-demonstration-recording-test/v1", "passed": checks})) + quit(0) diff --git a/pyproject.toml b/pyproject.toml index 39e625e..12805ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,8 @@ dvidia-skill-capsule = "dvidia_training.arm_entrypoints:capsule_main" dvidia-skillspace-coverage = "dvidia_training.coverage:main" dvidia-episode-tape = "dvidia_training.episode_tape:main" dvidia-action-demo = "dvidia_training.action_demonstration:main" +dvidia-demonstrate = "dvidia_training.godot_demonstration_bridge:main" +dvidia-demonstration-export = "dvidia_training.operator_export:main" dvidia-piper-capture = "dvidia_training.piper_action_capture:main" dvidia-reaction = "dvidia_training.reaction_benchmark:main" dvidia-motor-memory = "dvidia_training.motor_benchmark:main" diff --git a/scripts/check-operator-lifecycle.py b/scripts/check-operator-lifecycle.py new file mode 100644 index 0000000..f0853de --- /dev/null +++ b/scripts/check-operator-lifecycle.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Scripted local TCP lifecycle diagnostic; does not impersonate a person.""" +import argparse +import json +from pathlib import Path +import socket + +from dvidia_training import godot_demonstration_bridge as bridge +from dvidia_training import piper_action_capture as files + + +def check(scene,port): + info=json.loads((Path(scene)/'scene.json').read_text())['bridge'] + trace=[] + with socket.create_connection(('127.0.0.1',port),timeout=10) as connection: + stream=connection.makefile('rb') + def exchange(request): + connection.sendall((json.dumps(request)+'\n').encode()) + line=stream.readline(2*1024*1024) + if not line.endswith(b'\n'):raise ValueError('Truncated response.') + response=json.loads(line);trace.append(dict(request=request,response=response));return response + response=exchange(dict(type='hello',schema=bridge.SCHEMA,model_sha256=info['model_sha256'])) + assert response['recording']['source_kind']=='simulation_scripted_input' + target=info['home_target'] + step=lambda seq:dict(type='step',sequence=seq,target=target,jaw=.035) + assert exchange(step(1))['code']=='recording_status' + assert exchange(dict(type='begin',sequence=1))['state']['time']==0. + assert abs(exchange(step(2))['state']['time']-.02)<1e-10 + assert abs(exchange(dict(type='pause',sequence=3))['state']['time']-.02)<1e-10 + assert exchange(dict(type='ping'))['expected_sequence']==4 + assert exchange(step(4))['code']=='recording_status' + assert abs(exchange(dict(type='resume',sequence=4))['state']['time']-.02)<1e-10 + assert abs(exchange(step(5))['state']['time']-.04)<1e-10 + first=exchange(dict(type='finish',sequence=6)) + assert first['recording']['status']=='finished' and first['recording']['steps']==2 + reset=exchange(dict(type='reset',sequence=7)) + assert reset['state']['time']==0. and reset['recording']['status']=='ready' + assert first['recording']['output_directory']!=reset['recording']['output_directory'] + assert exchange(dict(type='begin',sequence=8))['state']['time']==0. + assert abs(exchange(step(9))['state']['time']-.02)<1e-10 + stream.close() + return dict(schema='dvidia.operator-lifecycle-check/v1',ok=True,trace=trace, + teacher_queries=0,human_demonstrations=0,physical_robot_ready=False, + scope='Scripted localhost control, pause/resume/reset and disconnect; no GUI button clicks.') + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--scene',required=True,type=Path) + parser.add_argument('--port',type=int,default=8431) + parser.add_argument('--output',required=True,type=Path) + args=parser.parse_args();result=check(args.scene,args.port) + files._json(args.output,result) + print(json.dumps(dict(ok=result['ok'],exchanges=len(result['trace']),output=str(args.output)))) + + +if __name__=='__main__':main() diff --git a/scripts/verify-operator-replay.py b/scripts/verify-operator-replay.py new file mode 100644 index 0000000..7a26c24 --- /dev/null +++ b/scripts/verify-operator-replay.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Independently execute recorded issued commands; compare native evidence. + +No teacher, camera rendering, fitting or physical commands. A failed task may +replay exactly: parity is not placement success or skill qualification. +""" +import argparse +from copy import deepcopy +from hashlib import sha256 +import json +from pathlib import Path +import time +from types import SimpleNamespace + +import numpy as np + +from dvidia_training import operator_capture as capture +from dvidia_training import camera_action_capture as camera +from dvidia_training import piper_action_capture as files +from dvidia_training.piper_feedback_env import FeedbackPiperTaskEnv +from dvidia_training.piper_feedback_score import score_feedback_episode +from dvidia_training.piper_task import PiperTaskConfig + + +def verify(assets,directory): + root=Path(directory) + inspection=capture.inspect_capture(root) + recording=capture.load_capture(root) + report=capture._read(root,'capture.json') + original=capture._read(root,'native_episode.json') + if report['uncommitted_native_intervals']: + raise ValueError('Only committed-prefix native evidence can be replayed by this verifier.') + config=PiperTaskConfig(**original['config']) + env=FeedbackPiperTaskEnv(assets,config) + began=time.perf_counter() + observation,info=env.reset(report['case']['seed']) + states=[deepcopy(observation)];actions=[] + for step in recording['steps']: + action=deepcopy(step['command']['issued']) + action_row=dict(t_seconds=step['before']['timestamp_seconds'],**action,phase=report['source_kind']) + states[-1].update(issued_joint_targets_rad=action['joint_targets_rad'], + issued_gripper_position_m=action['gripper_position_m'],command=action_row) + observation,info=env.step(action) + actions.append(action_row);states.append(deepcopy(observation)) + replay=camera._native_episode(SimpleNamespace(info=info,env=env,task_config=config),states,actions) + arrays=env.native_arrays();metadata=env.native_metadata() + comparison={};values=0;maximum=0.;exact=True + with np.load(root/'native.npz',allow_pickle=False) as saved: + if set(saved.files)!=set(arrays):raise ValueError('Native evidence field inventory differs.') + for key,actual in arrays.items(): + expected=saved[key] + if actual.shape!=expected.shape:raise ValueError('Native evidence shape differs: '+key) + error=float(np.max(np.abs(actual-expected))) if actual.size else 0. + matched=bool(np.array_equal(actual,expected)) + comparison[key]=dict(shape=list(actual.shape),max_abs_error=error,exact=matched) + values+=actual.size;maximum=max(maximum,error);exact=exact and matched + score=score_feedback_episode(replay,arrays,metadata,report['case']['goal_position_m'][:2]) + matches_score=score==report['score'] + return dict(schema='dvidia.operator-native-replay/v1',exact_native_arrays=exact, + native_values_compared=int(values),maximum_abs_error=maximum,fields=comparison, + independent_score_matches=matches_score,score=score, + original_strict_success=inspection['strict_success'],control_intervals=len(recording['steps']), + simulated_seconds=env.elapsed,wall_seconds=time.perf_counter()-began, + recording_sha256=sha256((root/'recording.json').read_bytes()).hexdigest(), + capture_integrity_sha256=sha256((root/'integrity.json').read_bytes()).hexdigest(), + source_kind=report['source_kind'],physical_robot_ready=False,teacher_queries=0, + scope='CPU native command replay including reset; excludes camera, Godot, fitting and hardware. ' + 'Native parity is separate from successful task completion.') + + +def main(): + parser=argparse.ArgumentParser(description=__doc__) + parser.add_argument('--assets',required=True,type=Path) + parser.add_argument('--capture',required=True,type=Path) + parser.add_argument('--output',required=True,type=Path) + args=parser.parse_args() + result=verify(args.assets,args.capture) + files._json(args.output,result) + print(json.dumps({key:result[key] for key in ('exact_native_arrays','native_values_compared', + 'maximum_abs_error','independent_score_matches','control_intervals','simulated_seconds')},indent=2)) + return 0 if result['exact_native_arrays'] and result['independent_score_matches'] else 1 + + +if __name__=='__main__':raise SystemExit(main()) diff --git a/src/dvidia_training/godot_demonstration_bridge.py b/src/dvidia_training/godot_demonstration_bridge.py new file mode 100644 index 0000000..1b0239c --- /dev/null +++ b/src/dvidia_training/godot_demonstration_bridge.py @@ -0,0 +1,346 @@ +"""Local, externally controlled PiPER block recording; no hardware commands. + +Godot is a view/controller. The camera-action native environment owns 1 ms +physics, 50 Hz commands and 25 Hz RGB-D. This protocol is separate from the +2 ms shoe workstation and never relabels its recordings. +""" +from __future__ import annotations + +import argparse +from copy import deepcopy +from hashlib import sha256 +import json +from pathlib import Path +import shutil +import socket +import time + +from . import asset_preview, camera_action_capture as camera, godot_bridge as wire +from . import reference_replay, reference_sim + +SCHEMA = 'dvidia.godot-demonstration/v1' +DEFAULT_PORT = 8431 +BOUNDS = ((.10, .50), (-.25, .25), (.06, .55)) +DEFAULT_CASE = dict(id='operator-block-001', group_id='operator-start-001', seed=0, + object_position_m=[.30, -.03, .082], goal_position_m=[.35, .08, .08]) +SCOPE = ('Simulation demonstration capture only. Fixed-orientation XYZ plus single-finger ' + 'target; 1 ms MuJoCo physics, 50 Hz controls, 25 Hz native RGB-D. ' + 'No hand tracking, trained policy, physical commands or hardware qualification.') +TIMING_SCOPE = 'Paused-simulator computation and capture; not physical reaction latency.' +MAX_EPISODES = 16 +RESERVE_BYTES = 10 * 1024**3 + + +def _case(path): + return camera._case(DEFAULT_CASE if path is None else json.loads(Path(path).read_text())) + + +def _state(native, target): + """Privileged viewer state. It is never an input to the pose controller.""" + env, mj, np = native.env, native.mj, native.np + quaternion = np.empty(4) + mj.mju_mat2Quat(quaternion, env.data.site_xmat[env.tcp]) + return dict(time=float(env.elapsed), native_time=float(env.data.time), + body_positions=env.data.xpos.tolist(), body_quaternions=env.data.xquat.tolist(), + qpos=env.data.qpos.tolist(), ctrl=env.data.ctrl.tolist(), + tcp_position=env.data.site_xpos[env.tcp].tolist(), tcp_quaternion=quaternion.tolist(), + object_position=env.data.xpos[env.block].tolist(), + position_error_m=float(np.linalg.norm(env.data.site_xpos[env.tcp]-target)), + scope='Viewer state includes native truth; excluded from learning inputs.') + + +def prepare(assets, output, *, case=None, duration_s=20.): + case = camera._case(DEFAULT_CASE if case is None else case) + config = camera.RolloutConfig(duration_s=duration_s, seed=case['seed']) + destination = Path(output).absolute() + if destination.exists() or destination.is_symlink() or any(p.is_symlink() for p in destination.parents): + raise ValueError('Prepared scene requires a fresh ordinary directory.') + provenance, notices = asset_preview._licenses('piper', assets) + native = camera._Session(assets, config, case) + try: + header, _, metadata = native.open() + model = native.env.model + digest = reference_sim.model_digest(model) + scene, geometry = asset_preview._scene(model, dict(robot='piper', assets=provenance, + mujoco=native.mj.__version__, compiled_model_sha256=digest)) + home = native.env.data.site_xpos[native.env.tcp].tolist() + if any(not low <= value <= high for value, (low, high) in zip(home, BOUNDS)): + raise ValueError('Initial TCP lies outside the declared operator envelope.') + scene['scope'] = SCOPE + scene['bridge'] = dict(schema=SCHEMA, model_sha256=digest, home_target=home, + bounds=[list(p) for p in BOUNDS], gripper_initial=.035, physics_dt=.001, + control_dt=.02, fixed_orientation_wxyz=list(native.task_config.tool_orientation_wxyz), + case=case, duration_s=duration_s, calibration_sha256=header['binding']['calibration_sha256'], + scope=SCOPE) + scene['geometry_binary'] = dict(href='geometry.bin', compression='none', + bytes=len(geometry), sha256=sha256(geometry).hexdigest()) + files = {'scene.json': wire._json(scene), 'geometry.bin': geometry, + 'LICENSE': reference_replay.MIT_LICENSE.encode(), **notices} + files['README.md'] = ('# DVIDIA demonstration scene\n\n'+SCOPE+'\n\n' + 'Prepared using the native block task reset/settling, not a zero-physics export. ' + 'Training RGB-D comes from the native calibrated camera, not the Godot viewport. ' + 'RGB-D calibration is simulated; it does not qualify physical transfer. ' + 'The upstream PiPER notices are in licenses/. No model weights are included.\n').encode() + seal = dict(schema=wire.INTEGRITY_SCHEMA, model_sha256=digest, + files={name:dict(bytes=len(body), sha256=sha256(body).hexdigest()) for name, body in files.items()}) + files['scene-integrity.json'] = wire._json(seal) + if (any(len(body)>asset_preview.MAX_FILE_BYTES for body in files.values()) + or sum(map(len,files.values()))>asset_preview.MAX_TOTAL_BYTES): + raise ValueError('Prepared geometry exceeds the existing export budget.') + destination.mkdir(parents=True, exist_ok=False) + for name, body in files.items(): + path = destination/name + path.parent.mkdir(parents=True, exist_ok=True) + with path.open('xb') as stream: + stream.write(body) + return dict(schema=SCHEMA, output=str(destination), model_sha256=digest, + bytes=sum(map(len,files.values())), native_reset_performed=True, + task_control_steps=0, physical_robot_ready=False, runtime=metadata['runtime']) + finally: + native.close() + + +class Session: + """One connection, bounded new episodes; no command loop without a client.""" + def __init__(self, factory, controller, *, model_sha256, home_target, max_episodes=4, + state_reader=_state): + if type(max_episodes) is not int or not 1 <= max_episodes <= MAX_EPISODES: + raise ValueError('Episode budget must be in [1,16].') + self.factory, self.controller, self.state_reader = factory, controller, state_reader + self.model_sha256, self.home_target = model_sha256, list(home_target) + self.max_episodes, self.episodes = max_episodes, 0 + self.expected_sequence, self.ready, self.faulted = 1, False, False + self.status, self.capture, self.last_report = 'ready', None, None + self.target, self.jaw = list(home_target), .035 + self.steps, self.max_steps = 0, 0 + self.controller_sources = {name:sha256((Path(__file__).parent/name).read_bytes()).hexdigest() + for name in ('godot_demonstration_bridge.py','piper_encoder_kinematics.py')} + self._new_capture() + + def _new_capture(self): + if self.episodes >= self.max_episodes: + raise wire.ProtocolError('Episode budget exhausted; start a new explicitly bounded session.', 'episode_budget') + capture = self.factory(self.episodes) + try: + opened = capture.open() + if opened['header']['binding']['model_sha256'] != self.model_sha256: + raise ValueError('Prepared and recording model digests differ.') + except BaseException: + capture.finish(reason='initialization_failed') + raise + self.capture = capture + self.episodes += 1 + self.max_steps = round(capture.config.duration_s/.02) + self.steps, self.status, self.last_report = 0, 'ready', None + self.target, self.jaw = list(self.home_target), .035 + self.last_state = self.state_reader(capture.session, self.target) + + def recording(self): + return dict(status=self.status, steps=self.steps, max_steps=self.max_steps, + source_kind=self.capture.source_kind, output_directory=str(self.capture.root), + episode=self.episodes, max_episodes=self.max_episodes, + physical_robot_ready=False, source_authenticated=False) + + def error(self, exc): + return dict(type='error', code=exc.code, message=str(exc), expected_sequence=self.expected_sequence) + + def _finish(self, reason): + report = self.capture.finish(reason=reason) + self.last_report = report + if (report.get('sealed') is not True or report.get('error') is not None + or report.get('native_evidence_saved') is not True): + raise RuntimeError('Capture could not seal complete native evidence; inspect retained files.') + return report + + def process(self, payload): + try: + return self.handle(wire.decode_request(payload)) + except wire.ProtocolError as exc: + return self.error(exc) + + def handle(self, request): + if self.faulted: + raise wire.ProtocolError('Session faulted; retained evidence may include an incomplete interval.', 'session_fault') + kind = request.get('type') + if not self.ready: + wire._fields(request, ('type','schema','model_sha256')) + if kind != 'hello' or request['schema'] != SCHEMA or request['model_sha256'] != self.model_sha256: + raise wire.ProtocolError('Prepared model and demonstration protocol must match.', 'handshake_required') + self.ready = True + return dict(type='ready', schema=SCHEMA, model_sha256=self.model_sha256, + physics_dt=.001, control_dt=.02, state=self.last_state, recording=self.recording(), scope=SCOPE) + if kind == 'ping': + wire._fields(request, ('type',)) + return dict(type='pong', model_sha256=self.model_sha256, expected_sequence=self.expected_sequence) + if kind not in ('begin','step','pause','resume','finish','reset'): + raise wire.ProtocolError('Unknown demonstration command.') + wire._fields(request, ('type','sequence','target','jaw') if kind=='step' else ('type','sequence')) + sequence = request['sequence'] + if type(sequence) is not int or sequence != self.expected_sequence or sequence > wire.MAX_SEQUENCE: + raise wire.ProtocolError('Expected the next command sequence.', 'sequence_mismatch') + allowed = {'begin':('ready',),'step':('recording',),'pause':('recording',), + 'resume':('paused',),'finish':('ready','recording','paused','finished'), + 'reset':('ready','recording','paused','finished')} + if self.status not in allowed[kind]: + raise wire.ProtocolError('Command does not match recording status.', 'recording_status') + if kind=='reset' and self.episodes >= self.max_episodes: + raise wire.ProtocolError('Episode budget exhausted.', 'episode_budget') + began = time.perf_counter() + target, jaw, action = self.target, self.jaw, None + if kind=='step': + if type(request['target']) is not list or len(request['target']) != 3: + raise wire.ProtocolError('Target requires three coordinates.') + target = [wire._number(x,*bounds,'Target') for x,bounds in zip(request['target'],BOUNDS)] + jaw = wire._number(request['jaw'],0.,.035,'Single-finger target') + # Only measured encoder state is provided to this isolated FK/IK + # controller; never the viewer's native object or contact truth. + try: + joints = self.capture.context()['snapshot']['signals']['joint_position_rad']['value'] + action = self.controller(joints,target,jaw) + except ValueError as exc: + raise wire.ProtocolError('Encoder-to-target mapping refused: '+str(exc),'control_refused') from exc + try: + if kind=='begin': + self.capture.record_event('begin') + self.status='recording' + elif kind=='pause': + self.capture.record_event('pause') + self.status='paused' + elif kind=='resume': + self.capture.record_event('resume') + self.status='recording' + elif kind=='step': + result = self.capture.step(action, input_event=dict(schema=SCHEMA, + sequence=sequence, target_world_m=target, gripper_single_finger_m=jaw, + input='fixed_orientation_cartesian', controller='encoder_only_dls', + controller_sources_sha256=self.controller_sources)) + self.steps += 1 + self.target,self.jaw = target,jaw + self.last_state = self.state_reader(self.capture.session,target) + if result['done']: + self._finish('capture_budget_complete') + self.status='finished' + elif kind=='finish': + if self.status!='finished': + self._finish('operator_stopped') + self.status='finished' + elif kind=='reset': + if self.status!='finished': + self.capture.record_event('reset') + self._finish('operator_reset') + self._new_capture() + except BaseException as exc: + self.faulted=True + try: + self.capture.finish(reason='session_fault') + except Exception: + pass # Original prefix remains recoverable; never retry a command. + if isinstance(exc,(KeyboardInterrupt,SystemExit)): + raise + raise wire.ProtocolError('Recording failed; inspect the retained prefix before starting a new episode.', 'recording_fault') from exc + self.expected_sequence += 1 + return dict(type='state',sequence=sequence,model_sha256=self.model_sha256, + state=self.last_state,recording=self.recording(), + step_compute_wall_seconds=time.perf_counter()-began,timing_scope=TIMING_SCOPE) + + def disconnect(self): + if self.status!='finished': + try: + self.capture.record_event('disconnect') + self._finish('client_disconnected') + self.status='finished' + except Exception: + self.faulted=True + self.status='faulted' + self.capture.session.close() + + +def serve(assets, scene, output, *, port=DEFAULT_PORT, duration_s=None, + source_kind='simulation_human_input', max_episodes=4): + from .operator_capture import OperatorCapture, SOURCE_KINDS + from .piper_encoder_kinematics import PiperEncoderKinematics + if type(port) is not int or not 1024 <= port <= 65535: + raise ValueError('Port must be an integer in [1024,65535].') + if source_kind not in SOURCE_KINDS: + raise ValueError('Unknown capture source kind.') + if type(max_episodes) is not int or not 1 <= max_episodes <= MAX_EPISODES: + raise ValueError('Episode budget must be in [1,16].') + scene = Path(scene) + seal = json.loads((scene/'scene-integrity.json').read_text()) + body = (scene/'scene.json').read_bytes() + if seal.get('files',{}).get('scene.json') != dict(bytes=len(body),sha256=sha256(body).hexdigest()): + raise ValueError('Prepared scene identity differs.') + bridge = json.loads(body)['bridge'] + if bridge['schema'] != SCHEMA or bridge['physics_dt'] != .001 or bridge['control_dt'] != .02: + raise ValueError('Unsupported prepared scene protocol or clocks.') + duration_s = bridge['duration_s'] if duration_s is None else duration_s + if duration_s != bridge['duration_s']: + raise ValueError('Duration must match prepared calibration; prepare a fresh scene for a different duration.') + config = camera.RolloutConfig(duration_s=duration_s, seed=bridge['case']['seed']) + root=Path(output).absolute() + if root.exists() or root.is_symlink() or any(p.is_symlink() for p in root.parents): + raise ValueError('Session output must be a fresh ordinary directory.') + parent=root.parent + while not parent.exists(): parent=parent.parent + upper=(round(duration_s/.04)+1)*(320*240*7+4096)+32*1024**2 + if shutil.disk_usage(parent).free < RESERVE_BYTES+max_episodes*upper: + raise ValueError('Capture capacity guard: preserve 10 GiB plus the declared episode budget.') + kinematics=PiperEncoderKinematics(assets) + def controller(joints,target,jaw): + return kinematics.pose_action(joints,target,bridge['fixed_orientation_wxyz'],jaw) + root.mkdir(parents=True,exist_ok=False) + def factory(index): + case=deepcopy(bridge['case']);case['id'] += '-episode-'+str(index+1) + return OperatorCapture(assets,root/f'episode-{index+1:04}',case=case, + duration_s=duration_s,source_kind=source_kind) + session=None + with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as listener: + listener.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) + listener.bind((wire.HOST,port));listener.listen(1) + print(json.dumps(dict(schema=SCHEMA,status='listening',host=wire.HOST,port=port, + output=str(root),source_kind=source_kind,max_episodes=max_episodes)),flush=True) + try: + connection,_=listener.accept() + with connection: + connection.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,1) + connection.settimeout(wire.IDLE_TIMEOUT_SECONDS) + session=Session(factory,controller,model_sha256=bridge['model_sha256'], + home_target=bridge['home_target'],max_episodes=max_episodes) + wire.handle_connection(connection,session) + finally: + if session is not None: session.disconnect() + + +def main(argv=None): + parser=argparse.ArgumentParser(description=__doc__) + sub=parser.add_subparsers(dest='command',required=True) + pre=sub.add_parser('prepare');pre.add_argument('--assets',required=True,type=Path) + pre.add_argument('--output',required=True,type=Path);pre.add_argument('--case',type=Path) + pre.add_argument('--duration',type=float,default=20.) + run=sub.add_parser('serve');run.add_argument('--assets',required=True,type=Path) + run.add_argument('--scene',required=True,type=Path);run.add_argument('--output',required=True,type=Path) + run.add_argument('--port',type=int,default=DEFAULT_PORT) + run.add_argument('--source-kind',choices=('simulation_human_input','simulation_scripted_input'),default='simulation_human_input') + run.add_argument('--max-episodes',type=int,default=4) + inspect=sub.add_parser('inspect');inspect.add_argument('directory',type=Path) + recover=sub.add_parser('recover');recover.add_argument('directory',type=Path) + args=parser.parse_args(argv) + try: + if args.command=='prepare': + result=prepare(args.assets,args.output,case=_case(args.case),duration_s=args.duration) + elif args.command=='serve': + serve(args.assets,args.scene,args.output,port=args.port,source_kind=args.source_kind,max_episodes=args.max_episodes) + return 0 + else: + from .operator_capture import inspect_capture,recover_capture + result=(inspect_capture if args.command=='inspect' else recover_capture)(args.directory) + print(json.dumps(result,indent=2)) + except KeyboardInterrupt: + return 0 + except (ValueError,OSError,RuntimeError) as exc: + parser.exit(2,str(exc)+'\n') + return 0 + + +if __name__=='__main__': + raise SystemExit(main()) diff --git a/src/dvidia_training/operator_capture.py b/src/dvidia_training/operator_capture.py new file mode 100644 index 0000000..450fd63 --- /dev/null +++ b/src/dvidia_training/operator_capture.py @@ -0,0 +1,756 @@ +"""Stateful external-command recording for the bounded native PiPER task. + +The public ``session`` is available for scene/model display binding only. The +caller supplies every seven-target command; this module never calls a teacher. +Human input is a producer assertion, not authenticated human provenance. +""" +from __future__ import annotations + +from copy import deepcopy +from dataclasses import asdict +from hashlib import sha256 +import io +import math +import os +from pathlib import Path +import time + +from . import action_demonstration as tape +from . import camera_action_capture as camera +from . import piper_action_capture as base +from . import piper_rgbd_tracker as tracker + +SCHEMA = 'dvidia.operator-capture/v1' +PERCEPTION_SCHEMA = SCHEMA + '/perception' +JOURNAL_SCHEMA = SCHEMA + '/journal' +SOURCE_KINDS = ('simulation_human_input', 'simulation_scripted_input') +MAX_DOCUMENT_BYTES = 64 * 1024 * 1024 +MAX_JOURNAL_BYTES = 16 * 1024 * 1024 +MAX_EVENTS = 10000 +SOURCE_FILES = ('operator_capture.py', 'camera_action_capture.py', 'piper_action_capture.py', + 'action_demonstration.py', 'piper_rgbd_tracker.py', 'piper_feedback_env.py', + 'piper_feedback_score.py', 'piper_task.py', 'piper_imitation_score.py', + 'piper_encoder_kinematics.py') +SCOPE = ('Native simulation with externally issued targets, 1 ms physics, ' + '50 Hz controls and native 25 Hz 320x240 RGB-D. Input provenance is ' + 'a producer assertion; no human authentication or hardware qualification.') + + +def _read(root, name, maximum=MAX_DOCUMENT_BYTES): + return tape._decode(tape._ordinary_bytes(root / str(tape._relative(name)), maximum)) + + +def _reference(root, name, document): + result = base._json(root / name, document) + result['path'] = name + return result + + +def _identity(root, name): + body = tape._ordinary_bytes(root / str(tape._relative(name)), MAX_DOCUMENT_BYTES) + return dict(bytes=len(body), sha256=sha256(body).hexdigest()) + + +def _artifact_name(root, path): + """Serialize local inventory paths using the platform-independent tape contract.""" + name = path.relative_to(root).as_posix() + tape._relative(name) + return name + + +def _detail(value): + if value is not None and type(value) is not dict: + raise ValueError('Input/event metadata must be a JSON object or null.') + body = tape.canonical(value) + if len(body) > 4096: + raise ValueError('Input/event metadata exceeds 4096 bytes.') + return tape._decode(body) + + +def _header(header): + if (header.get('schema') != SCHEMA or header.get('control_mode') != 'operator' + or header.get('provenance', {}).get('kind') not in SOURCE_KINDS + or header.get('source_authenticated') is not False + or header.get('human_authenticated') is not False): + raise ValueError('Invalid operator source provenance.') + # Validate unchanged native contracts using the strict v1 header validator. + # This local adapter is never serialized or used as a supervision label. + native = {name: deepcopy(header[name]) for name in tape.HEADER} + native['schema'] = tape.SCHEMA + native['provenance'].pop('case_id', None) + native['provenance']['kind'] = 'simulation_teleoperation' + tape._header(native) + if header.get('camera_cadence_seconds') != .04: + raise ValueError('Operator camera cadence must be 25 Hz.') + tape._sha(header.get('tracker_sha256'), 'tracker hash') + sources = header.get('operator_implementation_sha256') + if type(sources) is not dict or set(sources) != set(SOURCE_FILES): + raise ValueError('Operator implementation source identities are required.') + for name, value in sources.items(): + tape._sha(value, name) + if (header['provenance']['producer_revision'] != sources['operator_capture.py'] + or header['tracker_sha256'] != sources['piper_rgbd_tracker.py']): + raise ValueError('Operator/tracker source identity differs from implementation receipt.') + + +def _snapshot(snapshot, header, index, previous=None): + tape._snapshot(snapshot, header['calibration'], previous) + if not math.isclose(snapshot['timestamp_seconds'], index*tape.DT, + abs_tol=tape.CLOCK_TOL, rel_tol=0): + raise ValueError('Operator boundary must be a complete 20 ms interval.') + if any(snapshot['signals'][name]['status'] != 'missing' for name in tracker.SIGNALS): + raise ValueError('Raw snapshots cannot contain privileged object poses or tracker substitutions.') + frame = snapshot['camera']['value'] + if frame is None or frame['index'] != index//2: + raise ValueError('Camera frame cadence differs from 25 Hz.') + if not math.isclose(snapshot['camera']['captured_at_seconds'], (index//2)*.04, + abs_tol=tape.CLOCK_TOL, rel_tol=0): + raise ValueError('Camera acquisition time differs from native cadence.') + + +def _command(command, header, snapshot): + tape._keys(command, {'issued_at_seconds', 'requested', 'issued', 'authority', + 'operator_correction', 'issue_reason'}, 'operator command') + if (command['authority'] != header['provenance']['kind'] + or command['operator_correction'] is not False or command['issue_reason'] != 'unchanged' + or command['requested'] != command['issued'] + or command['issued_at_seconds'] != snapshot['timestamp_seconds']): + raise ValueError('Operator issued command authority differs.') + tape._action(command['issued'], header, 'issued command') + + +def _perception(root, reference, snapshot, header, index): + expected = {key: reference[key] for key in ('bytes', 'sha256')} + if _identity(root, reference['path']) != expected: + raise ValueError('Perception sidecar byte identity differs.') + item = _read(root, reference['path']) + if (item.get('schema') != PERCEPTION_SCHEMA or item.get('index') != index + or item.get('snapshot_sha256') != tape.digest(snapshot) + or item.get('frame') != snapshot['camera']['value'] + or item.get('tracker_sha256') != header['tracker_sha256'] + or item.get('calibration_sha256') != header['binding']['calibration_sha256'] + or item.get('input_calibration_sha256') != header['binding']['calibration_sha256']): + raise ValueError('Perception source snapshot/frame/calibration binding differs.') + estimate = item['estimate']; sensed = snapshot['camera'] + if (estimate.get('decision_at_seconds') != snapshot['timestamp_seconds'] + or estimate.get('frame_index') != snapshot['vision_frame_index'] + or estimate.get('status') not in ('available', 'missing', 'stale', 'invalid', 'refused')): + raise ValueError('Perception decision time or source frame differs.') + if estimate['status'] == 'available': + for field in ('captured_at_seconds', 'available_at_seconds', 'age_seconds'): + if estimate.get(field) != sensed[field]: + raise ValueError('Perception acquisition timing differs.') + for name in tracker.SIGNALS: + signal = estimate['signals'][name] + tape._availability(signal, snapshot['timestamp_seconds'], name) + expected_value = estimate[name] + if 'uncertainty' in name: + expected_value = [expected_value] + if (signal['status'] != 'available' or signal['value'] != expected_value + or any(signal[field] != estimate[field] for field in + ('captured_at_seconds', 'available_at_seconds', 'age_seconds'))): + raise ValueError('Perception signal value or timing differs.') + tape._vec(signal['value'], tape.SIGNALS[name]['size'], name) + if 'uncertainty' in name and signal['value'][0] < 0: + raise ValueError('Perception uncertainty cannot be negative.') + if 'quaternion' in name and not math.isclose(math.hypot(*signal['value']), 1., abs_tol=1e-6): + raise ValueError('Perception quaternion must be normalized.') + elif any(estimate.get(name) is not None or estimate.get('signals', {}).get(name) != base._missing() + for name in tracker.SIGNALS): + raise ValueError('Unavailable perception must preserve explicit missing masks.') + return item + + +class OperatorCapture: + """One exclusive attempt. ``step`` commits exactly one caller-issued interval. + + ``context`` returns only sensor/perception data, preceding applied targets, + and the declared goal. ``session`` is privileged display access and is not + part of that context. Call ``finish`` on stop/disconnect, including errors. + A failed step seals its committed prefix and re-raises; it never retries. + """ + def __init__(self, assets, output, *, case, duration_s=20., + source_kind='simulation_scripted_input'): + self.case = camera._case(case) + if source_kind not in SOURCE_KINDS: + raise ValueError('Choose simulation_human_input or simulation_scripted_input.') + self.config = camera.RolloutConfig(duration_s=duration_s, seed=self.case['seed']) + self.root = Path(output).absolute() + if any(item.is_symlink() and str(item) not in ('/var', '/tmp') + for item in (self.root, *self.root.parents)): + raise ValueError('Output cannot traverse symlinks.') + self.source_kind = source_kind + self.session = camera._Session(assets, self.config, self.case) + self.header = self.initial = self.snapshot = self.estimate = None + self.previous_applied = self.initial_targets = None + self.steps, self.states, self.actions = [], [], [] + self.started_intervals = self.frame_bytes = self.camera_pairs = 0 + self._journal = None; self._sequence = 0; self._last_hash = None + self._opened = self._sealed = False; self._report = None + self._started_wall = time.monotonic(); self._error = None + self._estimate_timing = {}; self._timings = [] + + @property + def done(self): + return self._sealed or len(self.steps) >= round(self.config.duration_s/tape.DT) + + def _append(self, kind, **payload): + # Reserve one bounded row so event flooding can still seal a refusal. + if self._sequence >= MAX_EVENTS-int(kind != 'finished'): + raise ValueError('Operator journal exceeds its bounded event count.') + record = dict(schema=JOURNAL_SCHEMA, sequence=self._sequence, + previous_sha256=self._last_hash, kind=kind, + simulation_seconds=self.snapshot['timestamp_seconds'] if self.snapshot else 0., + monotonic_wall_seconds=time.monotonic()-self._started_wall, **payload) + body = tape.canonical(record)+b'\n' + if self._journal.tell()+len(body) > MAX_JOURNAL_BYTES-(1024 if kind != 'finished' else 0): + raise ValueError('Operator journal exceeds its byte budget.') + self._journal.write(body); self._journal.flush(); os.fsync(self._journal.fileno()) + self._sequence += 1; self._last_hash = sha256(body).hexdigest() + return record + + def record_event(self, name, detail=None): + if not self._opened or self._sealed: + raise RuntimeError('Events require an open, unfinished capture.') + if type(name) is not str or not name or len(name) > 80: + raise ValueError('Event name must contain 1 to 80 characters.') + return deepcopy(self._append('event', name=name, detail=_detail(detail))) + + def _save_frames(self, frames): + additional = sum(len(body) for body in frames.values()) + if self.frame_bytes+additional > tape.MAX_FRAME_TOTAL: + raise ValueError('Unique native RGB-D frames exceed 512 MiB.') + for name, body in frames.items(): + tape._relative(name) + if not name.startswith('frames/') or len(body) > tape.MAX_FRAME_BYTES: + raise ValueError('Native frame path or size is invalid.') + base._record(self.root/name, body); self.frame_bytes += len(body) + if frames: + self.camera_pairs += 1 + + def _save_perception(self, index): + estimate = camera._estimate_at(self.estimate, self.snapshot['timestamp_seconds']) + return _reference(self.root, f'perception/{index:04}.json', dict( + schema=PERCEPTION_SCHEMA, index=index, snapshot_sha256=tape.digest(self.snapshot), + frame=deepcopy(self.snapshot['camera']['value']), estimate=estimate, + tracker_sha256=self.header['tracker_sha256'], + calibration_sha256=self.header['binding']['calibration_sha256'], + input_calibration_sha256=self._estimate_timing['input_calibration_sha256'])) + + def open(self): + if self._opened or self._sealed: + raise RuntimeError('A capture can only be opened once.') + self.root.mkdir(parents=True, exist_ok=False) + base._json(self.root/'started.json', dict(schema=SCHEMA, status='started', + case=self.case, config=asdict(self.config), source_kind=self.source_kind, scope=SCOPE)) + self._journal = (self.root/'commands.jsonl').open('xb') + self._opened = True + try: + native_header, targets, metadata = self.session.open() + tape._header(native_header) + self.header = deepcopy(native_header) + self.header.update(schema=SCHEMA, control_mode='operator', camera_cadence_seconds=.04, + source_authenticated=False, human_authenticated=False, + tracker_sha256=sha256(Path(tracker.__file__).read_bytes()).hexdigest()) + self.header['provenance'].update(kind=self.source_kind, source_group=self.case['group_id'], + case_id=self.case['id']) + source_hashes = {name: sha256((Path(__file__).parent/name).read_bytes()).hexdigest() + for name in SOURCE_FILES} + self.header['provenance']['producer_revision'] = source_hashes['operator_capture.py'] + self.header['operator_implementation_sha256'] = source_hashes + metadata = deepcopy(metadata) + metadata.update(operator_implementation_sha256=source_hashes, + tracker_implemented=True, operator_corrections_collected=False, + policy_observation_origin='Privileged diagnostics only; excluded from operator context.', + command_origin='External input supplied to OperatorCapture.step; no teacher queries.') + self.header['goal_position_m'] = deepcopy(self.case['goal_position_m']) + self.initial_targets = self.previous_applied = deepcopy(targets) + self.snapshot, frames, diagnostic, timing = self.session.sample(0) + _snapshot(self.snapshot, self.header, 0) + self.initial = deepcopy(self.snapshot) + self.estimate, self._estimate_timing = camera._fresh_estimate( + self.session, self.snapshot, self.header['calibration']) + self._save_frames(frames) + self._perception_ref = self._save_perception(0) + _perception(self.root, self._perception_ref, self.snapshot, self.header, 0) + base._json(self.root/'native/0000.json', diagnostic) + self.states.append(deepcopy(diagnostic['policy_observation'])) + base._json(self.root/'context.json', dict(header=self.header, initial=self.initial, + initial_applied_targets=self.initial_targets, metadata=metadata, case=self.case, + initial_perception=self._perception_ref)) + self._timings.append({**timing, **self._estimate_timing}) + self.record_event('opened') + return dict(context=self.context(), header=deepcopy(self.header), + snapshot=deepcopy(self.snapshot), estimate=deepcopy(self.estimate)) + except (Exception, KeyboardInterrupt) as exc: + self._error = dict(type=type(exc).__name__, message=str(exc)[:500]) + self.finish('open_error') + raise + + def context(self): + if self.snapshot is None or self.estimate is None: + raise RuntimeError('Capture has no initialized sensor boundary.') + return dict(snapshot=deepcopy(self.snapshot), + estimate=camera._estimate_at(self.estimate, self.snapshot['timestamp_seconds']), + previous_applied=deepcopy(self.previous_applied), + goal_position_m=deepcopy(self.case['goal_position_m'])) + + def step(self, action, *, input_event=None): + if not self._opened or self.done: + raise RuntimeError('Capture is not open or has reached its interval budget.') + tape._action(action, self.header, 'operator-issued targets') + action = deepcopy(action); input_event = _detail(input_event) + index = len(self.steps); before = deepcopy(self.snapshot) + command = dict(issued_at_seconds=before['timestamp_seconds'], requested=deepcopy(action), + issued=deepcopy(action), authority=self.source_kind, operator_correction=False, + issue_reason='unchanged') + try: + decision = dict(index=index, command=command, perception=self._perception_ref, + input_event=input_event, snapshot_sha256=tape.digest(before)) + decision_ref = _reference(self.root, f'decisions/{index:04}.json', decision) + self._append('intent', index=index, decision=decision_ref) + self.started_intervals += 1 + applied = self.session.advance(action) + next_index = index+1 + if next_index % 2 == 0: + after, frames, diagnostic, timing = self.session.sample(next_index//2) + estimate, estimate_timing = camera._fresh_estimate( + self.session, after, self.header['calibration']) + else: + after, frames, diagnostic, timing = self.session.measured(before['camera']) + estimate, estimate_timing = self.estimate, self._estimate_timing + _snapshot(after, self.header, next_index, before) + self._save_frames(frames) + tape._action(applied['ramped_targets'], self.header, 'applied targets') + if (applied['interval_start_seconds'] != before['timestamp_seconds'] + or applied['interval_end_seconds'] != after['timestamp_seconds']): + raise ValueError('Native applied interval differs from the observed 20 ms boundary.') + step = dict(index=index, before=before, command=command, applied=deepcopy(applied), + after=deepcopy(after), perception=deepcopy(self._perception_ref), + input_event=input_event, teacher_query=None) + native_ref = _reference(self.root, f'native/{next_index:04}.json', diagnostic) + transition_ref = _reference(self.root, f'transitions/{index:04}.json', step) + # Outcome is the commit marker. A transition without this marker is + # retained as evidence, but is never included in a recovered prefix. + self._append('outcome', index=index, status='committed', + transition=transition_ref, native=native_ref) + self.steps.append(step) + action_row = dict(t_seconds=before['timestamp_seconds'], **action, phase=self.source_kind) + self.states[-1].update(issued_joint_targets_rad=action['joint_targets_rad'], + issued_gripper_position_m=action['gripper_position_m'], command=action_row) + self.actions.append(action_row); self.states.append(deepcopy(diagnostic['policy_observation'])) + self.snapshot = deepcopy(after); self.estimate = estimate; self._estimate_timing = estimate_timing + self.previous_applied = deepcopy(applied['ramped_targets']) + self._timings.append(timing) + self._perception_ref = self._save_perception(next_index) + return dict(snapshot=deepcopy(self.snapshot), estimate=self.context()['estimate'], + applied=deepcopy(applied), done=self.done, context=self.context()) + except (Exception, KeyboardInterrupt) as exc: + self._error = dict(type=type(exc).__name__, message=str(exc)[:500]) + self.finish('producer_interrupted' if isinstance(exc, KeyboardInterrupt) else 'producer_error') + raise + + def _reconcile(self): + """Rebuild counters and native control trace from durable commit markers. + + An interrupt can occur after fsync and before *any* Python assignment, + including the journal sequence/hash cursor. Memory is never the source + of truth for finalization. + """ + self._journal.flush(); os.fsync(self._journal.fileno()) + _, context, steps, intents, _, finished = _prefix(self.root) + entries, _ = _journal(self.root) + body = tape._ordinary_bytes(self.root/'commands.jsonl', MAX_JOURNAL_BYTES) + self._sequence = len(entries) + self._last_hash = sha256(body.splitlines(keepends=True)[-1]).hexdigest() if entries else None + self._journal.seek(0, os.SEEK_END) + self.started_intervals = len(intents) + self.steps = deepcopy(steps) + self.states, self.actions = [], [] + if context is None: + # A sampled frame in memory does not establish an initialized + # recording. Without context there is no bound native initial state. + self.initial = self.snapshot = self.estimate = None + self.initial_targets = self.previous_applied = None + else: + self.header = deepcopy(context['header']); self.initial = deepcopy(context['initial']) + self.initial_targets = deepcopy(context['initial_applied_targets']) + self.states.append(deepcopy(_read(self.root, 'native/0000.json')['policy_observation'])) + outcomes = [entry for entry in entries if entry['kind'] == 'outcome'] + for step, outcome in zip(steps, outcomes): + action = deepcopy(step['command']['issued']) + row = dict(t_seconds=step['before']['timestamp_seconds'], **action, phase=self.source_kind) + self.states[-1].update(issued_joint_targets_rad=action['joint_targets_rad'], + issued_gripper_position_m=action['gripper_position_m'], command=row) + self.actions.append(row) + self.states.append(deepcopy(_bound_document(self.root, outcome['native'])['policy_observation'])) + self.snapshot = deepcopy(steps[-1]['after'] if steps else self.initial) + self.previous_applied = deepcopy(steps[-1]['applied']['ramped_targets'] if steps else self.initial_targets) + perception_path = f'perception/{len(steps):04}.json' + if (self.root/perception_path).exists(): + self.estimate = _read(self.root, perception_path)['estimate'] + else: + sensed = self.snapshot['camera'] + self.estimate = tracker._empty(self.snapshot['vision_frame_index'], + sensed['captured_at_seconds'], sensed['available_at_seconds'], self.snapshot['timestamp_seconds']) + self.estimate['reason'] = 'terminal_perception_not_committed' + frames = [path for path in (self.root/'frames').glob('*') if path.is_file()] + self.frame_bytes = sum(path.stat().st_size for path in frames) + rgb = {path.name.removeprefix('rgb-').split('.',1)[0] for path in frames if path.name.startswith('rgb-')} + depth = {path.name.removeprefix('depth-').split('.',1)[0] for path in frames if path.name.startswith('depth-')} + self.camera_pairs = len(rgb & depth) + return finished + + def _leave_unsealed(self, exc, reason): + """Close resources and retain an explicit recovery requirement on disk.""" + failure = dict(type=type(exc).__name__, message=str(exc)[:500], stage='finish_reconciliation') + try: + self.session.close() + except (Exception, KeyboardInterrupt) as close_error: + failure['renderer_close_error'] = str(close_error)[:500] + try: + self._journal.close() + except (Exception, KeyboardInterrupt) as close_error: + failure['journal_close_error'] = str(close_error)[:500] + report = dict(schema=SCHEMA, status='interrupted', sealed=False, + reason='journal_reconciliation_failed', requested_finish_reason=reason, + error=self._error, finish_error=failure, source_kind=self.source_kind, case=self.case, + recorded_complete_intervals=None, started_intervals=None, + uncommitted_native_intervals=None, recovery_required=True, + source_authenticated=False, human_authenticated=False, physical_robot_ready=False, + teacher_queries=0, score=_no_score(), strict_success=False) + try: + base._json(self.root/'finish_error.json', report) + except (Exception, KeyboardInterrupt) as write_error: + report['finish_error']['receipt_write_error'] = str(write_error)[:500] + self._sealed = True; self._report = report + return deepcopy(report) + + def finish(self, reason='operator_stopped'): + if self._sealed: + return deepcopy(self._report) + if not self._opened: + self.session.close() + self._report = dict(schema=SCHEMA, status='refused', sealed=False, + reason='capture_not_opened', source_kind=self.source_kind, case=self.case, + requested_intervals=round(self.config.duration_s/tape.DT), started_intervals=0, + recorded_complete_intervals=0, uncommitted_native_intervals=0, + source_authenticated=False, human_authenticated=False, physical_robot_ready=False, + teacher_queries=0, score=_no_score(), strict_success=False) + self._sealed = True + return deepcopy(self._report) + if type(reason) is not str or not reason or len(reason) > 200: + raise ValueError('Finish reason must contain 1 to 200 characters.') + try: + already_finished = self._reconcile() + except (Exception, KeyboardInterrupt) as exc: + # Never seal an internally inconsistent journal/recording pair. + # Recovery remains read-only and may inspect a valid flushed prefix. + return self._leave_unsealed(exc, reason) + native_status = {}; native_saved = False + try: + native_status = self.session.status() + if self.initial is not None: + import numpy as np + arrays, metadata = self.session.native_evidence() + buffer = io.BytesIO(); np.savez_compressed(buffer, **arrays) + base._record(self.root/'native.npz', buffer.getvalue()) + base._json(self.root/'native_metadata.json', metadata) + base._json(self.root/'native_episode.json', camera._native_episode(self.session, self.states, self.actions)) + native_saved = True + except Exception as exc: + self._error = self._error or dict(type=type(exc).__name__, message=str(exc)[:500]) + reason = 'native_evidence_error' if self._error['message'] == str(exc)[:500] else reason + finally: + try: + self.session.close() + except Exception as exc: + self._error = self._error or dict(type=type(exc).__name__, message=str(exc)[:500]) + initialized = (self.root/'context.json').exists() + complete = initialized and self._error is None and len(self.steps) == round(self.config.duration_s/tape.DT) + status = 'complete' if complete else 'interrupted' if initialized else 'refused' + if complete: + reason = 'capture_budget_complete' + try: + score = camera._score(self.root, self.case['goal_position_m']) if native_saved else _no_score() + except Exception as exc: + score = dict(valid=False, success=False, reason='native_score_error', error=str(exc)[:500]) + if initialized: + recording = dict(**self.header, initial=self.initial, + initial_applied_targets=self.initial_targets, steps=self.steps, + outcome=dict(status=status, reason=reason, + end_seconds=self.steps[-1]['after']['timestamp_seconds'] if self.steps else 0., + task_result='unverified' if complete else 'unknown')) + base._json(self.root/'recording.json', recording) + if not already_finished: + self._append('finished', reason=reason, status=status) + self._journal.close() + report = dict(schema=SCHEMA, status=status, reason=reason, sealed=True, + error=self._error, source_kind=self.source_kind, case=self.case, scope=SCOPE, + source_authenticated=False, human_authenticated=False, physical_robot_ready=False, + teacher_queries=0, requested_intervals=round(self.config.duration_s/tape.DT), + started_intervals=self.started_intervals, recorded_complete_intervals=len(self.steps), + uncommitted_native_intervals=self.started_intervals-len(self.steps), + uncommitted_interval_scope='Intent without a commit may have advanced zero, partial, or full native ticks.', + camera_pairs=self.camera_pairs, frame_bytes=self.frame_bytes, + initial_anchor_available=bool(initialized and + _read(self.root, 'perception/0000.json')['estimate']['status'] == 'available'), + native_status=native_status, native_evidence_saved=native_saved, + score=score, strict_success=bool(complete and score.get('success')), + timing=dict(monotonic_wall_seconds=time.monotonic()-self._started_wall, + scope='Paused-simulation acquisition; no wall-clock delivery guarantee.')) + base._json(self.root/'capture.json', report) + names = [_artifact_name(self.root, path) for path in sorted(self.root.rglob('*')) if path.is_file()] + files = {name: _identity(self.root, name) for name in names} + base._json(self.root/'integrity.json', dict(schema=SCHEMA+'/integrity', files=files)) + self._sealed = True; self._report = report + return deepcopy(report) + + +def _no_score(): + return dict(valid=False, success=False, reason='native_evidence_unavailable') + + +def _journal(root, *, allow_truncated=False): + body = tape._ordinary_bytes(root/'commands.jsonl', MAX_JOURNAL_BYTES) + lines = body.splitlines(keepends=True); records = []; last_hash = None + tail_bytes = 0 + for line in lines: + if not line.endswith(b'\n'): + if not allow_truncated: + raise ValueError('Command journal has an unflushed final record.') + tail_bytes = len(line); break + item = tape._decode(line) + if (item.get('schema') != JOURNAL_SCHEMA or item.get('sequence') != len(records) + or item.get('previous_sha256') != last_hash or len(records) >= MAX_EVENTS): + raise ValueError('Command journal hash chain or order differs.') + tape._bounded(item.get('monotonic_wall_seconds'), 'journal wall time', 0, 1e9) + tape._time(item.get('simulation_seconds'), 'journal simulation time') + if records and item['monotonic_wall_seconds'] < records[-1]['monotonic_wall_seconds']: + raise ValueError('Journal wall time cannot run backwards.') + last_hash = sha256(line).hexdigest(); records.append(item) + return records, tail_bytes + + +def _bound_document(root, reference): + if _identity(root, reference['path']) != {key: reference[key] for key in ('bytes', 'sha256')}: + raise ValueError('Journal artifact byte identity differs.') + return _read(root, reference['path']) + + +def _prefix(root, *, allow_truncated=False): + started = _read(root, 'started.json') + if started.get('schema') != SCHEMA or started.get('source_kind') not in SOURCE_KINDS: + raise ValueError('Unknown operator recording source.') + config = camera.RolloutConfig(**started['config']) + entries, tail_bytes = _journal(root, allow_truncated=allow_truncated) + context = _read(root, 'context.json') if (root/'context.json').exists() else None + if context is not None: + _header(context['header']) + tape._action(context['initial_applied_targets'], context['header'], 'initial applied targets') + steps = []; intents = []; pending = None; finished = False + for entry in entries: + if finished: + raise ValueError('Journal continues after finish.') + kind = entry['kind'] + if kind == 'intent': + if pending is not None or entry['index'] != len(steps) or context is None: + raise ValueError('Command intents must follow the committed prefix once.') + pending = _bound_document(root, entry['decision']) + if pending['index'] != len(steps): + raise ValueError('Decision index differs from command intent.') + boundary = steps[-1]['after'] if steps else context['initial'] + _command(pending['command'], context['header'], boundary) + _detail(pending['input_event']) + if pending['snapshot_sha256'] != tape.digest(boundary): + raise ValueError('Pending intent differs from its causal sensor boundary.') + _perception(root, pending['perception'], boundary, context['header'], len(steps)) + intents.append(pending) + elif kind == 'outcome': + if pending is None or entry['index'] != len(steps) or entry.get('status') != 'committed': + raise ValueError('Command outcome has no matching pending intent.') + step = _bound_document(root, entry['transition']) + _bound_document(root, entry['native']) + if (step['index'] != len(steps) or step['command'] != pending['command'] + or step['perception'] != pending['perception'] + or step['input_event'] != pending['input_event'] + or tape.digest(step['before']) != pending['snapshot_sha256']): + raise ValueError('Committed transition differs from recorded command intent.') + steps.append(step); pending = None + elif kind == 'event': + if type(entry.get('name')) is not str or not 1 <= len(entry['name']) <= 80: + raise ValueError('Invalid operator event name.') + _detail(entry.get('detail')) + elif kind == 'finished': + finished = True + else: + raise ValueError('Unknown command journal event.') + if len(intents) > round(config.duration_s/tape.DT): + raise ValueError('Operator capture exceeds its scheduled interval budget.') + if context is not None: + header = context['header']; _header(header) + if (header['provenance']['kind'] != started['source_kind'] + or header['provenance']['source_group'] != started['case']['group_id'] + or header['provenance']['case_id'] != started['case']['id']): + raise ValueError('Operator provenance differs from its started attempt.') + previous = context['initial']; _snapshot(previous, header, 0) + _perception(root, context['initial_perception'], previous, header, 0) + for index, step in enumerate(steps): + if step['before'] != previous or step.get('teacher_query') is not None: + raise ValueError('Operator prefix continuity or teacher separation differs.') + _snapshot(step['after'], header, index+1, previous) + command = step['command'] + _command(command, header, step['before']) + applied = step['applied'] + tape._action(applied['ramped_targets'], header, 'applied targets') + controls = tape._vec(applied['native_position_controls'], 7, 'native controls', -10, 10) + tape._action(dict(joint_targets_rad=controls[:6], gripper_position_m=controls[6]), header, 'native controls') + if (applied['interval_start_seconds'] != step['before']['timestamp_seconds'] + or applied['interval_end_seconds'] != step['after']['timestamp_seconds'] + or applied['scope'] != 'final_native_tick_only' or type(applied['rate_limited']) is not bool): + raise ValueError('Applied interval differs from the committed boundary.') + _perception(root, step['perception'], step['before'], header, index) + previous = step['after'] + # Both the initial and final snapshots bind their native frame bytes. + for snapshot in [context['initial'], *[step['after'] for step in steps]]: + for channel in ('rgb', 'depth'): + reference = snapshot['camera']['value'][channel] + if _identity(root, reference['path']) != {key: reference[key] for key in ('bytes', 'sha256')}: + raise ValueError('Camera source byte binding differs.') + return started, context, steps, intents, tail_bytes, finished + + +def recover_capture(directory): + """Read-only recovery of an unsealed committed prefix; never resume or seal. + + An outstanding intent cannot establish how many native ticks ran. Even a + full scheduled prefix remains interrupted until an ordinary finish sealed + its native evidence. Recovered rows cannot be passed to the exporter. + """ + root = Path(directory) + if (root/'integrity.json').exists(): + raise ValueError('Capture is already sealed; inspect or load it normally.') + started, context, steps, intents, tail, _ = _prefix(root, allow_truncated=True) + report = dict(schema=SCHEMA+'/recovery', status='interrupted', sealed=False, + reason='process_interrupted', source_kind=started['source_kind'], + recorded_complete_intervals=len(steps), started_intervals=len(intents), + uncommitted_native_intervals=len(intents)-len(steps), truncated_journal_bytes=tail, + native_progress_of_uncommitted_interval='unknown', strict_success=False, + score=_no_score(), source_authenticated=False, physical_robot_ready=False) + recording = None if context is None else dict(**context['header'], initial=context['initial'], + initial_applied_targets=context['initial_applied_targets'], steps=steps, + outcome=dict(status='interrupted', reason='process_interrupted', task_result='unknown', + end_seconds=steps[-1]['after']['timestamp_seconds'] if steps else 0.)) + return dict(report=report, recording=recording) + + +def _native_bindings(root, steps, report): + """Bind emitted targets, encoder inputs, and applied references to native evidence.""" + if not report['native_evidence_saved']: + return + episode = _read(root, 'native_episode.json') + if 'actions' not in episode: + if report['native_status'].get('native_initialized'): + raise ValueError('Initialized native recording lacks native action evidence.') + return # Explicit non-physics fixtures cannot establish strict success. + import numpy as np + import zipfile + with zipfile.ZipFile(root/'native.npz') as archive: + if sum(item.file_size for item in archive.infolist()) > MAX_DOCUMENT_BYTES: + raise ValueError('Expanded native arrays exceed the bounded evidence budget.') + actions, states = episode['actions'], episode['trajectory'] + if len(actions) != len(steps) or len(states) != len(steps)+1: + raise ValueError('Native action/state trace differs from the committed prefix.') + with np.load(root/'native.npz', allow_pickle=False) as archive: + arrays = {name: archive[name] for name in ('arm_actuator_controls_rad', + 'applied_jaw_target_m', 'normal_rate_limited_jaw_target_m', + 'joint_position_rad', 'gripper_joint_position_m', 'requested_jaw_target_m')} + widths = {'arm_actuator_controls_rad':6, 'joint_position_rad':6, + 'gripper_joint_position_m':2} + for name, values in arrays.items(): + expected_tail = (widths[name],) if name in widths else () + if (values.dtype.kind not in 'fiu' or values.shape[1:] != expected_tail + or not len(steps)*20 <= len(values) <= report['requested_intervals']*20 + or not np.isfinite(values).all()): + raise ValueError('Native array shape or finite-value contract differs: '+name) + for index, step in enumerate(steps): + if {name: actions[index][name] for name in ('joint_targets_rad', 'gripper_position_m')} != step['command']['issued']: + raise ValueError('Issued command differs from native action evidence.') + for position, snapshot in ((index, step['before']), (index+1, step['after'])): + for name in ('joint_position_rad', 'joint_velocity_rad_s', 'gripper_joint_position_m'): + if snapshot['signals'][name]['value'] != states[position][name]: + raise ValueError('Measured input differs from native encoder evidence.') + boundary = (index+1)*20-1 + controls = [*arrays['arm_actuator_controls_rad'][boundary].tolist(), + float(arrays['applied_jaw_target_m'][boundary])] + ramped = dict(joint_targets_rad=controls[:6], + gripper_position_m=float(arrays['normal_rate_limited_jaw_target_m'][boundary])) + if step['applied']['native_position_controls'] != controls or step['applied']['ramped_targets'] != ramped: + raise ValueError('Applied inputs differ from native servo reference evidence.') + for name in ('joint_position_rad', 'gripper_joint_position_m'): + if not np.allclose(arrays[name][boundary], step['after']['signals'][name]['value'], + atol=2e-7, rtol=0): + raise ValueError('After-state encoders differ from native tick evidence.') + if not np.allclose(arrays['requested_jaw_target_m'][index*20:(index+1)*20], + step['command']['issued']['gripper_position_m'], atol=1e-12, rtol=0): + raise ValueError('Issued jaw target differs from native tick evidence.') + + +def inspect_capture(directory): + """Verify the sealed journal, causal rows, byte bindings and independent score.""" + root = Path(directory); manifest = _read(root, 'integrity.json') + if manifest.get('schema') != SCHEMA+'/integrity' or type(manifest.get('files')) is not dict: + raise ValueError('An operator recording requires a sealed integrity manifest.') + observed = {_artifact_name(root, path) for path in root.rglob('*') + if path.is_file() and path != root/'integrity.json'} + if observed != set(manifest['files']): + raise ValueError('Operator recording inventory differs.') + for name, identity in manifest['files'].items(): + if _identity(root, name) != identity: + raise ValueError('Operator recording byte identity differs: '+name) + frame_identities = [identity for name, identity in manifest['files'].items() if name.startswith('frames/')] + frame_bytes = sum(identity['bytes'] for identity in frame_identities) + if frame_bytes > tape.MAX_FRAME_TOTAL or any(identity['bytes'] > tape.MAX_FRAME_BYTES for identity in frame_identities): + raise ValueError('Native RGB-D frame evidence exceeds its byte budget.') + started, context, steps, intents, _, finished = _prefix(root) + report = _read(root, 'capture.json') + entries, _ = _journal(root) + if (report.get('schema') != SCHEMA or report.get('sealed') is not True or not finished + or report.get('status') not in ('complete', 'interrupted', 'refused') + or report.get('source_kind') != started['source_kind'] + or report.get('teacher_queries') != 0 or report.get('source_authenticated') is not False + or report.get('human_authenticated') is not False + or report.get('physical_robot_ready') is not False + or report.get('case') != started['case'] + or report.get('requested_intervals') != round(started['config']['duration_s']/tape.DT) + or report.get('frame_bytes') != frame_bytes + or report.get('recorded_complete_intervals') != len(steps) + or report.get('started_intervals') != len(intents) + or report.get('uncommitted_native_intervals') != len(intents)-len(steps)): + raise ValueError('Operator report disagrees with the command journal.') + if entries[-1].get('status') != report['status'] or entries[-1].get('reason') != report['reason']: + raise ValueError('Operator report differs from the journal finish record.') + if context is not None: + recording = _read(root, 'recording.json') + if (any(recording.get(key) != value for key, value in context['header'].items()) + or recording.get('initial') != context['initial'] + or recording.get('initial_applied_targets') != context['initial_applied_targets'] + or recording.get('steps') != steps + or recording['outcome']['status'] != report['status']): + raise ValueError('Sealed recording differs from its committed prefix.') + if report['status'] == 'complete' and (len(steps) != round(started['config']['duration_s']/tape.DT) + or len(intents) != len(steps) or report.get('error') is not None): + raise ValueError('Complete recording omits scheduled intervals or contains an error.') + _native_bindings(root, steps, report) + score = camera._score(root, started['case']['goal_position_m']) if report['native_evidence_saved'] else _no_score() + if score != report['score'] or report['strict_success'] != bool(report['status'] == 'complete' and score.get('success')): + raise ValueError('Independent native score differs from the sealed report.') + return dict(schema=SCHEMA+'/inspection', status=report['status'], sealed=True, + files_verified=len(manifest['files']), recorded_complete_intervals=len(steps), + source_authenticated=False, human_authenticated=False, physical_robot_ready=False, + score=score, strict_success=report['strict_success']) + + +def load_capture(directory): + inspect_capture(directory) + return _read(Path(directory), 'recording.json') diff --git a/src/dvidia_training/operator_export.py b/src/dvidia_training/operator_export.py new file mode 100644 index 0000000..4b333dc --- /dev/null +++ b/src/dvidia_training/operator_export.py @@ -0,0 +1,226 @@ +"""Explicitly reviewed external commands as direct camera-action supervision. + +Selections name original control-row indices. History is computed causally from +all preceding sensor rows, then selected; gaps never reset or fabricate history. +Failed attempts remain usable with an explicit review reason. No labels are +inferred from native outcomes, teacher suggestions, or zero residuals. +""" +from __future__ import annotations + +import argparse +from hashlib import sha256 +import io +import json +from pathlib import Path + +import numpy as np + +from . import camera_action_model as model +from . import operator_capture as capture + +STORAGE_SCHEMA = 'dvidia.operator-dataset/v1' +MAX_ARRAY_BYTES = 16 * 1024 * 1024 + + +def _selection(count, reviewed_indices, reviewed_segments): + if (reviewed_indices is None) == (reviewed_segments is None): + raise ValueError('Choose exactly one explicit reviewed row-index or segment selection.') + if reviewed_indices is not None: + if type(reviewed_indices) not in (list, tuple): + raise ValueError('Reviewed indices must be an explicit list of integers.') + selected = list(reviewed_indices) + else: + if type(reviewed_segments) not in (list, tuple) or not reviewed_segments: + raise ValueError('Reviewed segments must be an explicit list of [start, stop) pairs.') + selected = [] + for segment in reviewed_segments: + if (type(segment) not in (list, tuple) or len(segment) != 2 + or any(type(value) is not int for value in segment) + or not 0 <= segment[0] < segment[1] <= count): + raise ValueError('Reviewed segments use bounded, half-open [start, stop) row indices.') + selected.extend(range(*segment)) + if not selected or any(type(index) is not int or not 0 <= index < count for index in selected): + raise ValueError('Every reviewed index must identify a committed control row.') + if len(set(selected)) != len(selected): + raise ValueError('Duplicate reviewed indices or overlapping segments are forbidden.') + return sorted(selected) + + +def export_episode(directory, *, reviewed_indices=None, reviewed_segments=None, + review_reason=None, blind=False): + """Export sealed, explicitly selected rows with exact issued seven-target labels. + + ``reviewed_segments`` uses half-open ``[start, stop)`` index pairs. The + nonempty review reason is retained even for unsuccessful/interrupted runs. + This records the caller's review assertion, not authenticated human review. + """ + if type(review_reason) is not str or not review_reason.strip() or len(review_reason) > 2000: + raise ValueError('An explicit nonempty review reason is required for every export.') + if type(blind) is not bool: + raise ValueError('blind must be boolean.') + root = Path(directory) + recording = capture.load_capture(root) + recording_body = capture.tape._ordinary_bytes(root/'recording.json', capture.MAX_DOCUMENT_BYTES) + if recording_body.strip() != model.canonical(recording): + raise ValueError('Recording changed after operator verification.') + if (recording.get('schema') != capture.SCHEMA or recording.get('control_mode') != 'operator' + or recording.get('provenance', {}).get('kind') not in capture.SOURCE_KINDS): + raise ValueError('Reviewed operator exports require operator source provenance.') + selected = _selection(len(recording['steps']), reviewed_indices, reviewed_segments) + selected_set = set(selected) + encoder = model.CameraActionFeatures(blind=blind) + previous_applied = recording['initial_applied_targets'] + features, targets, perception_hashes, authorities = [], [], [], [] + for index, step in enumerate(recording['steps']): + if index > selected[-1]: + break + item = capture._perception(root, step['perception'], step['before'], recording, index) + feature = encoder(dict(snapshot=step['before'], estimate=item['estimate'], + previous_applied=previous_applied, goal_position_m=recording['goal_position_m'])) + if index in selected_set: + if (step['command']['authority'] != recording['provenance']['kind'] + or step.get('teacher_query') is not None): + raise ValueError('Operator labels cannot impersonate student or teacher authority.') + features.append(feature) + targets.append(model.action_vector(step['command']['issued'])) + perception_hashes.append(step['perception']['sha256']) + authorities.append(step['command']['authority']) + previous_applied = step['applied']['ramped_targets'] + report = capture._read(root, 'capture.json') + group = recording['provenance']['source_group'] + source = dict(source_group=group, case_id=recording['provenance']['case_id'], + source_kind=recording['provenance']['kind'], source_authenticated=False, + recording_sha256=sha256(recording_body).hexdigest(), + integrity_sha256=sha256((root/'integrity.json').read_bytes()).hexdigest(), + calibration_sha256=recording['binding']['calibration_sha256'], + tracker_sha256=recording['tracker_sha256'], label_source='operator_issued', + sample_count=len(selected), reviewed_indices=selected, review_reason=review_reason.strip(), + review_authenticated=False, attempt_status=report['status'], + attempt_strict_success=report['strict_success'], + perception_references_sha256=model.digest(perception_hashes), + actual_authority_sha256=model.digest(authorities)) + sources = [source] + provenance = dict(feature_contract_sha256=model.FEATURE_CONTRACT_SHA256, blind=blind, + episodes=sources, source_identity_sha256=model.digest(sources), + export_source_sha256=sha256(Path(__file__).read_bytes()).hexdigest(), + review_selection_sha256=model.digest(dict(indices=selected, reason=review_reason.strip())), + label_definition='absolute targets actually issued by the declared external operator source') + dataset = dict(schema=model.DATASET_SCHEMA, features=np.asarray(features, dtype=np.float32), + targets=np.asarray(targets, dtype=np.float64), groups=[group]*len(selected), provenance=provenance) + model._dataset(dataset, blind=blind) + return dataset + + +def _validated_dataset(dataset): + if type(dataset) is not dict: + raise ValueError('A reviewed operator dataset dictionary is required.') + provenance = dataset.get('provenance', {}) + if type(provenance.get('blind')) is not bool: + raise ValueError('Operator dataset requires explicit feedback masking provenance.') + x, y, groups, provenance, _ = model._dataset(dataset, blind=provenance['blind']) + sources = provenance.get('episodes') + if (provenance.get('feature_contract_sha256') != model.FEATURE_CONTRACT_SHA256 + or type(sources) is not list or len(sources) != 1 + or provenance.get('source_identity_sha256') != model.digest(sources)): + raise ValueError('Operator dataset requires one bound reviewed source episode.') + source = sources[0] + indices = source.get('reviewed_indices') + if (source.get('source_kind') not in capture.SOURCE_KINDS + or source.get('source_authenticated') is not False + or source.get('review_authenticated') is not False + or source.get('label_source') != 'operator_issued' + or type(source.get('review_reason')) is not str or not source['review_reason'].strip() + or type(indices) is not list or len(indices) != len(groups) + or indices != _selection(1000, indices, None) + or source.get('sample_count') != len(groups) + or any(group != source.get('source_group') for group in groups)): + raise ValueError('Operator dataset review, authority, or source-group provenance differs.') + if provenance.get('review_selection_sha256') != model.digest( + dict(indices=indices, reason=source['review_reason'])): + raise ValueError('Operator review selection binding differs.') + return x, y, groups, provenance + + +def save_dataset(dataset, output): + """Save one reviewed export to a fresh directory, with exclusive array files.""" + x, y, groups, provenance = _validated_dataset(dataset) + root = Path(output).absolute() + if any(item.is_symlink() and str(item) not in ('/var', '/tmp') + for item in (root, *root.parents)): + raise ValueError('Dataset output cannot traverse symlinks.') + buffers = {} + for name, values in (('features', np.asarray(x, dtype=np.float32)), + ('targets', np.asarray(y, dtype=np.float64))): + buffer = io.BytesIO(); np.save(buffer, values, allow_pickle=False) + body = buffer.getvalue() + if len(body) > MAX_ARRAY_BYTES: + raise ValueError('Reviewed dataset array exceeds its bounded storage budget.') + buffers[name] = body + root.mkdir(parents=True, exist_ok=False) + files = {} + for name, body in buffers.items(): + filename = name+'.npy' + files[name] = capture.base._record(root/filename, body) + document = dict(storage_schema=STORAGE_SCHEMA, schema=model.DATASET_SCHEMA, + groups=groups, provenance=provenance, files=files) + capture.base._json(root/'dataset.json', document) + return root/'dataset.json' + + +def load_dataset(directory): + """Read bounded non-pickle arrays and verify reviewed source provenance.""" + root = Path(directory) + if {path.name for path in root.iterdir()} != {'dataset.json','features.npy','targets.npy'}: + raise ValueError('Reviewed dataset inventory differs.') + document = capture._read(root, 'dataset.json', maximum=2*1024*1024) + if (set(document) != {'storage_schema','schema','groups','provenance','files'} + or document['storage_schema'] != STORAGE_SCHEMA or document['schema'] != model.DATASET_SCHEMA + or type(document['groups']) is not list or not 1 <= len(document['groups']) <= 1000 + or type(document['files']) is not dict or set(document['files']) != {'features','targets'}): + raise ValueError('Unknown or unbounded reviewed operator dataset.') + arrays = {} + for name, width in (('features', model.FEATURE_COUNT), ('targets', 7)): + reference = document['files'][name] + if type(reference) is not dict or set(reference) != {'path','bytes','sha256'} or reference['path'] != name+'.npy': + raise ValueError('Dataset arrays require their exact ordinary relative paths.') + body = capture.tape._ordinary_bytes(root/reference['path'], MAX_ARRAY_BYTES) + if reference['bytes'] != len(body) or reference['sha256'] != sha256(body).hexdigest(): + raise ValueError('Dataset array byte identity differs.') + stream = io.BytesIO(body) + version = np.lib.format.read_magic(stream) + reader = {(1,0):np.lib.format.read_array_header_1_0, + (2,0):np.lib.format.read_array_header_2_0}.get(version) + if reader is None: + raise ValueError('Unsupported reviewed array format.') + shape, fortran, dtype = reader(stream) + if (shape != (len(document['groups']), width) or fortran or dtype.kind != 'f' + or dtype.itemsize not in (4,8) or dtype.hasobject + or stream.tell()+int(np.prod(shape))*dtype.itemsize != len(body)): + raise ValueError('Dataset array shape, numeric dtype, or payload size differs.') + arrays[name] = np.load(io.BytesIO(body), allow_pickle=False) + dataset = dict(schema=document['schema'], **arrays, + groups=document['groups'], provenance=document['provenance']) + _validated_dataset(dataset) + return dataset + + +def main(argv=None): + parser = argparse.ArgumentParser(description='Export explicitly reviewed native operator rows.') + parser.add_argument('--capture', required=True, help='Sealed operator capture directory') + parser.add_argument('--review', required=True, help='JSON with indices OR segments, plus reason') + parser.add_argument('--output', required=True, help='Fresh output dataset directory') + parser.add_argument('--blind', action='store_true', help='Mask live feedback using the existing feature contract') + args = parser.parse_args(argv) + review = capture.tape._decode(capture.tape._ordinary_bytes(Path(args.review), 64*1024)) + if type(review) is not dict or set(review) not in ({'indices','reason'}, {'segments','reason'}): + parser.error('Review JSON requires exactly indices OR segments, plus reason.') + dataset = export_episode(args.capture, reviewed_indices=review.get('indices'), + reviewed_segments=review.get('segments'), review_reason=review['reason'], blind=args.blind) + result = save_dataset(dataset, args.output) + print(json.dumps(dict(dataset=str(result), reviewed_rows=len(dataset['groups']), + source_authenticated=False, training_started=False))) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/tests/test_godot_demonstration_bridge.py b/tests/test_godot_demonstration_bridge.py new file mode 100644 index 0000000..4fe4e54 --- /dev/null +++ b/tests/test_godot_demonstration_bridge.py @@ -0,0 +1,125 @@ +"""Transport/authority tests with test doubles; no native execution.""" +from copy import deepcopy +import json +from pathlib import Path +from types import SimpleNamespace +import unittest + +from dvidia_training import godot_demonstration_bridge as bridge + +DIGEST='a'*64 +HOME=[.3,0.,.25] + + +class Capture: + def __init__(self,index): + self.root=Path('/not-written')/str(index) + self.config=SimpleNamespace(duration_s=.1) + self.source_kind='simulation_scripted_input' + self.session=SimpleNamespace(env=SimpleNamespace(elapsed=0.)) + self.events=[];self.actions=[];self.reasons=[];self.fail=False;self.finish_error=False + def open(self): + return dict(header=dict(binding=dict(model_sha256=DIGEST))) + def context(self): + return dict(snapshot=dict(signals=dict(joint_position_rad=dict(value=[0.]*6))), + native_truth='must not reach controller') + def step(self,action,*,input_event=None): + self.session.env.elapsed+=.02 + if self.fail:raise RuntimeError('partial native advance') + self.actions.append((deepcopy(action),deepcopy(input_event))) + return dict(done=len(self.actions)>=5) + def record_event(self,name,detail=None):self.events.append(name) + def finish(self,reason='operator_stopped'): + self.reasons.append(reason) + return dict(reason=reason,sealed=True,native_evidence_saved=not self.finish_error, + error='disk failure' if self.finish_error else None) + + +class BridgeTests(unittest.TestCase): + def setUp(self): + self.captures=[];self.calls=[] + def factory(index): + capture=Capture(index);self.captures.append(capture);return capture + def controller(joints,target,jaw): + self.calls.append((joints,target,jaw)) + return dict(joint_targets_rad=joints,gripper_position_m=jaw) + self.session=bridge.Session(factory,controller,model_sha256=DIGEST,home_target=HOME, + max_episodes=2,state_reader=lambda native,target:dict(time=native.env.elapsed,target=target)) + def call(self,kind,**kw): + if kind not in ('hello','ping'):kw.setdefault('sequence',self.session.expected_sequence) + return self.session.process(json.dumps(dict(type=kind,**kw)).encode()) + def hello(self):return self.call('hello',schema=bridge.SCHEMA,model_sha256=DIGEST) + def step(self,**kw):return self.call('step',target=kw.pop('target',HOME),jaw=kw.pop('jaw',.035),**kw) + def test_explicit_record_and_causal_controller_inputs(self): + self.assertEqual(self.step()['type'],'error') + self.assertEqual(self.hello()['recording']['status'],'ready') + self.assertEqual(self.step()['code'],'recording_status') + self.call('begin') + response=self.step() + self.assertEqual(response['state']['time'],.02) + self.assertEqual(self.calls,[([0.]*6,HOME,.035)]) + action,event=self.captures[0].actions[0] + self.assertEqual(event['target_world_m'],HOME) + self.assertEqual(action['gripper_position_m'],.035) + def test_bad_requests_never_advance_or_consume_sequence(self): + self.hello();self.call('begin');sequence=self.session.expected_sequence + cases=[dict(target=[.1,.2]),dict(target=[100,0,.2]),dict(jaw=True),dict(jaw=.1),dict(sequence=0)] + for case in cases: + self.assertEqual(self.step(**case)['type'],'error') + self.assertEqual(self.session.expected_sequence,sequence) + for raw in (b'{"type":"step","type":"ping"}',b'[]',b'{"type":NaN}'): + self.assertEqual(self.session.process(raw)['type'],'error') + self.assertEqual(self.captures[0].session.env.elapsed,0.) + self.assertEqual(self.calls,[]) + def test_pause_ping_resume_and_finish_do_not_advance(self): + self.hello();self.call('begin');self.step() + self.call('pause');sequence=self.session.expected_sequence + self.assertEqual(self.step()['code'],'recording_status') + self.assertEqual(self.call('ping')['expected_sequence'],sequence) + self.assertEqual(self.session.expected_sequence,sequence) + self.call('resume');self.call('finish') + self.assertEqual(self.captures[0].session.env.elapsed,.02) + self.assertEqual(self.captures[0].events,['begin','pause','resume']) + self.assertEqual(self.session.status,'finished') + self.call('finish') + self.assertEqual(self.captures[0].reasons,['operator_stopped']) + def test_reset_seals_old_episode_and_budget_refuses_without_mutation(self): + self.hello();self.call('begin');self.step();response=self.call('reset') + self.assertEqual(response['state']['time'],0.) + self.assertEqual(response['recording']['status'],'ready') + self.assertEqual(self.captures[0].reasons,['operator_reset']) + self.assertNotEqual(self.captures[0].root,self.captures[1].root) + self.assertEqual(self.call('reset')['code'],'episode_budget') + self.assertEqual(self.captures[1].reasons,[]) + def test_partial_native_interval_faults_and_never_retries(self): + self.hello();self.call('begin');self.captures[0].fail=True + self.assertEqual(self.step()['code'],'recording_fault') + self.assertEqual(self.step()['code'],'session_fault') + self.assertEqual(self.captures[0].session.env.elapsed,.02) + self.assertEqual(self.captures[0].reasons,['session_fault']) + def test_budget_finish_and_disconnect_have_truthful_authority(self): + self.hello();self.call('begin') + for _ in range(5):self.step() + self.assertEqual(self.session.status,'finished') + self.assertEqual(self.captures[0].reasons,['capture_budget_complete']) + self.session.disconnect() + self.assertEqual(len(self.captures[0].reasons),1) + self.call('reset');self.session.disconnect() + self.assertEqual(self.captures[1].events,['disconnect']) + self.assertEqual(self.captures[1].reasons,['client_disconnected']) + def test_wrong_model_and_protocol_are_rejected(self): + for values in (dict(schema='old',model_sha256=DIGEST),dict(schema=bridge.SCHEMA,model_sha256='b'*64)): + self.assertEqual(self.call('hello',**values)['type'],'error') + self.assertFalse(self.session.ready) + self.assertEqual(self.captures[0].actions,[]) + + def test_failed_sealing_is_not_reported_as_saved_and_reset_does_not_hide_it(self): + self.hello();self.call('begin');self.step() + self.captures[0].finish_error=True + self.assertEqual(self.call('reset')['code'],'recording_fault') + self.assertTrue(self.session.faulted) + self.assertEqual(len(self.captures),1) + self.assertEqual(self.step()['code'],'session_fault') + + +if __name__=='__main__':unittest.main() diff --git a/tests/test_operator_capture.py b/tests/test_operator_capture.py new file mode 100644 index 0000000..2953bf8 --- /dev/null +++ b/tests/test_operator_capture.py @@ -0,0 +1,311 @@ +"""Operator recording tests use sensor fixtures, never native physics.""" +from copy import deepcopy +from hashlib import sha256 +import json +from pathlib import Path, PurePosixPath, PureWindowsPath +from tempfile import TemporaryDirectory +import unittest +from unittest.mock import patch + +from dvidia_training import action_demonstration as tape +from dvidia_training import camera_action_capture as camera +from dvidia_training import operator_capture as capture +from dvidia_training import piper_rgbd_tracker as tracker +from tests.test_camera_action_capture import FakeSession, fake_estimate, CASE, STUDENT + + +class OperatorCaptureTests(unittest.TestCase): + def setUp(self): + FakeSession.instances = []; FakeSession.fail_at = None + self.temp = TemporaryDirectory(); self.addCleanup(self.temp.cleanup) + for target, value in (('_Session', FakeSession), ('_fresh_estimate', fake_estimate), + ('_native_episode', lambda *args: {'test_only': True}), + ('_score', lambda *args: dict(valid=False, success=False, reason='fixture_not_physics'))): + self.enterContext(patch.object(camera, target, value)) + + def new(self, name='capture', **kwargs): + return capture.OperatorCapture('unused', Path(self.temp.name)/name, case=CASE, + duration_s=kwargs.pop('duration_s', .2), **kwargs) + + def complete(self, name='capture', **kwargs): + recorder = self.new(name, **kwargs); opened = recorder.open() + while not recorder.done: + recorder.step(deepcopy(STUDENT), input_event={'kind': 'test-key'}) + report = recorder.finish() + return recorder, opened, report + + def test_inventory_names_are_portable_without_normalizing_imported_references(self): + windows_root = PureWindowsPath('D:/recordings/operator-run') + posix_root = PurePosixPath('/recordings/operator-run') + for root in (windows_root, posix_root): + name = capture._artifact_name(root, root/'frames'/'rgb-0000.png') + self.assertEqual(name, 'frames/rgb-0000.png') + self.assertEqual(str(tape._relative(name)), name) + self.assertEqual(capture._artifact_name(root, root/'native'/'0001.json'), 'native/0001.json') + with self.assertRaises(ValueError): + capture._artifact_name(windows_root, PureWindowsPath('D:/outside/0001.json')) + with self.assertRaisesRegex(ValueError, 'relative path'): + capture._identity(Path(self.temp.name), r'frames\rgb-0000.png') + + def test_external_commands_cadence_and_no_teacher(self): + recorder, opened, report = self.complete(source_kind='simulation_human_input') + self.assertEqual(report['status'], 'complete') + self.assertEqual(report['camera_pairs'], 6) + self.assertEqual(report['recorded_complete_intervals'], 10) + self.assertEqual(report['teacher_queries'], 0) + self.assertEqual(recorder.session.teacher_calls, 0) + self.assertTrue(recorder.session.closed) + self.assertFalse(report['human_authenticated']) + recording = capture.load_capture(recorder.root) + for index, step in enumerate(recording['steps']): + self.assertEqual(step['command']['issued'], STUDENT) + self.assertEqual(step['command']['authority'], 'simulation_human_input') + self.assertIsNone(step['teacher_query']) + self.assertAlmostEqual(step['after']['timestamp_seconds'], (index+1)*.02) + self.assertEqual(step['before']['camera']['value']['index'], index//2) + for name in tracker.SIGNALS: + self.assertEqual(step['before']['signals'][name]['status'], 'missing') + self.assertFalse((recorder.root/'teacher').exists()) + self.assertFalse(report['strict_success']) + with self.assertRaises(RuntimeError): + recorder.step(STUDENT) + self.assertEqual(recorder.finish(), report) + + def test_context_is_deep_copied_sensor_whitelist_and_events_do_not_advance(self): + recorder = self.new(); opened = recorder.open() + self.assertEqual(set(opened['context']), {'snapshot','estimate','previous_applied','goal_position_m'}) + self.assertNotIn('policy_observation', json.dumps(opened['context'])) + self.assertNotIn('native_info', json.dumps(opened['context'])) + recorder.record_event('pause', {'reason':'review'}) + recorder.record_event('resume') + self.assertEqual(recorder.session.position, 0) + opened['context']['previous_applied']['joint_targets_rad'][0] = 9. + self.assertNotEqual(recorder.context()['previous_applied']['joint_targets_rad'][0], 9.) + returned = recorder.step(STUDENT) + self.assertEqual(returned['context']['previous_applied'], returned['applied']['ramped_targets']) + report = recorder.finish('operator_stopped') + self.assertEqual(report['status'], 'interrupted') + entries, _ = capture._journal(recorder.root) + self.assertEqual([e['name'] for e in entries if e['kind']=='event'], ['opened','pause','resume']) + capture.inspect_capture(recorder.root) + + def test_partial_native_interval_seals_only_committed_prefix_without_retry(self): + FakeSession.fail_at = 'advance' + recorder = self.new(); recorder.open() + recorder.step(STUDENT); recorder.step(STUDENT) + with self.assertRaisesRegex(RuntimeError, 'partial interval'): + recorder.step(STUDENT) + report = recorder.finish() + self.assertEqual(report['started_intervals'], 3) + self.assertEqual(report['recorded_complete_intervals'], 2) + self.assertEqual(report['uncommitted_native_intervals'], 1) + self.assertEqual(report['status'], 'interrupted') + self.assertEqual(recorder.session.position, 3) + self.assertTrue(recorder.session.closed) + capture.inspect_capture(recorder.root) + + def test_readonly_crash_recovery_does_not_claim_native_completion(self): + recorder = self.new(); recorder.open(); recorder.step(STUDENT) + command = dict(issued_at_seconds=.02, requested=deepcopy(STUDENT), issued=deepcopy(STUDENT), + authority=recorder.source_kind, operator_correction=False, issue_reason='unchanged') + decision = dict(index=1, command=command, perception=recorder._perception_ref, + input_event=None, snapshot_sha256=tape.digest(recorder.snapshot)) + ref = capture._reference(recorder.root, 'decisions/0001.json', decision) + recorder._append('intent', index=1, decision=ref) + recorder._journal.close(); recorder.session.close() + before = {str(p.relative_to(recorder.root)): p.read_bytes() for p in recorder.root.rglob('*') if p.is_file()} + recovered = capture.recover_capture(recorder.root) + self.assertEqual(recovered['report']['recorded_complete_intervals'], 1) + self.assertEqual(recovered['report']['uncommitted_native_intervals'], 1) + self.assertFalse(recovered['report']['sealed']) + self.assertFalse(recovered['report']['strict_success']) + self.assertEqual(len(recovered['recording']['steps']), 1) + after = {str(p.relative_to(recorder.root)): p.read_bytes() for p in recorder.root.rglob('*') if p.is_file()} + self.assertEqual(before, after) + with self.assertRaises(FileNotFoundError): + capture.load_capture(recorder.root) + + def test_recovery_discards_incomplete_final_journal_line(self): + recorder = self.new(); recorder.open(); recorder.step(STUDENT) + recorder._journal.write(b'{"unfinished":'); recorder._journal.flush() + recorder._journal.close(); recorder.session.close() + result = capture.recover_capture(recorder.root) + self.assertEqual(result['report']['recorded_complete_intervals'], 1) + self.assertGreater(result['report']['truncated_journal_bytes'], 0) + + def test_transition_without_journal_commit_is_not_recovered(self): + recorder = self.new(); recorder.open() + append = recorder._append + def abrupt_exit(kind, **payload): + if kind == 'outcome': + raise SystemExit('simulated process loss before commit') + return append(kind, **payload) + with patch.object(recorder, '_append', abrupt_exit), self.assertRaises(SystemExit): + recorder.step(STUDENT) + recorder._journal.close(); recorder.session.close() + self.assertTrue((recorder.root/'transitions/0000.json').exists()) + recovered = capture.recover_capture(recorder.root) + self.assertEqual(recovered['report']['recorded_complete_intervals'], 0) + self.assertEqual(recovered['report']['uncommitted_native_intervals'], 1) + + def test_interrupt_after_durable_outcome_rebuilds_native_trace_before_sealing(self): + recorder = self.new(); recorder.open() + append = recorder._append; trace = [] + def interrupt_after_outcome(kind, **payload): + result = append(kind, **payload) + if kind == 'outcome': + raise KeyboardInterrupt('after durable outcome, before memory update') + return result + def native_episode(session, states, actions): + trace.append((deepcopy(states), deepcopy(actions))) + return {'test_only':True} + with patch.object(recorder, '_append', interrupt_after_outcome), \ + patch.object(camera, '_native_episode', side_effect=native_episode), \ + self.assertRaises(KeyboardInterrupt): + recorder.step(STUDENT) + report = recorder.finish() + self.assertEqual(report['status'], 'interrupted') + self.assertEqual(report['recorded_complete_intervals'], 1) + self.assertEqual(report['started_intervals'], 1) + self.assertEqual(report['uncommitted_native_intervals'], 0) + self.assertEqual(len(trace[0][0]), 2) + self.assertEqual(len(trace[0][1]), 1) + self.assertEqual(trace[0][1][0]['joint_targets_rad'], STUDENT['joint_targets_rad']) + capture.inspect_capture(recorder.root) + + def test_interrupt_after_durable_intent_rebuilds_started_counter(self): + recorder = self.new(); recorder.open() + append = recorder._append + def interrupt_after_intent(kind, **payload): + result = append(kind, **payload) + if kind == 'intent': + raise KeyboardInterrupt('after durable intent, before started counter') + return result + with patch.object(recorder, '_append', interrupt_after_intent), self.assertRaises(KeyboardInterrupt): + recorder.step(STUDENT) + report = recorder.finish() + self.assertEqual(report['started_intervals'],1) + self.assertEqual(report['recorded_complete_intervals'],0) + self.assertEqual(report['uncommitted_native_intervals'],1) + self.assertEqual(recorder.session.position,0) + capture.inspect_capture(recorder.root) + + def test_interrupt_after_journal_fsync_reconciles_cursor_before_finished_append(self): + for kind in ('intent','outcome'): + with self.subTest(kind=kind): + recorder = self.new(kind); recorder.open() + journal_fd = recorder._journal.fileno() + original_fsync = capture.os.fsync + append = recorder._append; armed = [False]; interrupted = [False] + def arm_before_append(actual_kind, **payload): + armed[0] = actual_kind == kind and not interrupted[0] + return append(actual_kind, **payload) + def interrupt_after_fsync(fd): + original_fsync(fd) + if fd == journal_fd and armed[0]: + armed[0] = False; interrupted[0] = True + raise KeyboardInterrupt('journal fsync returned before cursor assignment') + with patch.object(recorder, '_append', arm_before_append), \ + patch.object(capture.os, 'fsync', interrupt_after_fsync), \ + self.assertRaises(KeyboardInterrupt): + recorder.step(STUDENT) + report = recorder.finish() + self.assertTrue(report['sealed']) + self.assertEqual(report['started_intervals'],1) + self.assertEqual(report['recorded_complete_intervals'],int(kind == 'outcome')) + entries, tail = capture._journal(recorder.root) + self.assertEqual(tail,0) + self.assertEqual([entry['sequence'] for entry in entries],list(range(len(entries)))) + self.assertEqual(entries[-1]['kind'],'finished') + capture.inspect_capture(recorder.root) + + def test_truncated_journal_during_failure_remains_unsealed_for_readonly_recovery(self): + recorder = self.new(); recorder.open() + append = recorder._append + def interrupt_mid_outcome(kind, **payload): + if kind == 'outcome': + recorder._journal.write(b'{"partial_outcome":') + recorder._journal.flush() + raise KeyboardInterrupt('mid journal record') + return append(kind, **payload) + with patch.object(recorder, '_append', interrupt_mid_outcome), self.assertRaises(KeyboardInterrupt): + recorder.step(STUDENT) + report = recorder.finish() + self.assertFalse(report['sealed']) + self.assertEqual(report['finish_error']['stage'],'finish_reconciliation') + self.assertTrue(recorder.session.closed) + self.assertTrue((recorder.root/'finish_error.json').exists()) + self.assertFalse((recorder.root/'integrity.json').exists()) + self.assertFalse((recorder.root/'recording.json').exists()) + recovered = capture.recover_capture(recorder.root) + self.assertEqual(recovered['report']['recorded_complete_intervals'],0) + self.assertEqual(recovered['report']['uncommitted_native_intervals'],1) + self.assertFalse(recovered['report']['strict_success']) + + def test_sidecar_tampering_and_missing_masks_rejected(self): + recorder, _, _ = self.complete() + path = recorder.root/'perception/0000.json' + document = json.loads(path.read_text()); document['estimate']['status'] = 'missing' + body = tape.canonical(document)+b'\n'; path.write_bytes(body) + with self.assertRaisesRegex(ValueError, 'byte identity'): + capture.inspect_capture(recorder.root) + with self.assertRaisesRegex(ValueError, 'missing masks'): + capture._perception(recorder.root, + dict(path='perception/0000.json', bytes=len(body), sha256=sha256(body).hexdigest()), + recorder.initial, recorder.header, 0) + + def test_missing_live_camera_estimate_is_retained_without_native_truth(self): + def missing_after_initial(session, snapshot, calibration, negative=None): + return fake_estimate(session, snapshot, calibration, + 'initial_missing' if snapshot['timestamp_seconds'] >= .04 else None) + with patch.object(camera, '_fresh_estimate', missing_after_initial): + recorder, _, _ = self.complete() + recording = capture.load_capture(recorder.root) + item = capture._read(recorder.root, recording['steps'][2]['perception']['path']) + self.assertEqual(item['estimate']['status'], 'missing') + self.assertIsNone(item['estimate']['object_position_estimate_m']) + + def test_validation_and_exclusive_output(self): + for changes in ({'duration_s':21.}, {'duration_s':.11}, {'source_kind':'teacher'}, + {'source_kind':'simulation_teleoperation'}): + with self.assertRaises(ValueError): + self.new(**changes) + recorder = self.new(); recorder.open() + with self.assertRaises(ValueError): + recorder.step({**STUDENT, 'gripper_position_m':.04}) + with self.assertRaises(ValueError): + recorder.record_event('oversized', {'data':'x'*4097}) + self.assertEqual(recorder.session.position, 0) + recorder.finish() + with self.assertRaises(FileExistsError): + self.new().open() + + def test_setup_failure_is_sealed_and_closed(self): + FakeSession.fail_at = 'open'; recorder = self.new() + with self.assertRaises(RuntimeError): + recorder.open() + self.assertEqual(recorder.finish()['status'], 'refused') + self.assertTrue(recorder.session.closed) + capture.inspect_capture(recorder.root) + + def test_estimator_initialization_failure_closes_and_seals_refusal(self): + recorder = self.new() + with patch.object(camera, '_fresh_estimate', side_effect=RuntimeError('estimator initialization')): + with self.assertRaisesRegex(RuntimeError, 'estimator initialization'): + recorder.open() + self.assertTrue(recorder.session.closed) + self.assertEqual(recorder.finish()['status'], 'refused') + self.assertFalse(recorder.finish()['native_evidence_saved']) + self.assertFalse((recorder.root/'native_episode.json').exists()) + capture.inspect_capture(recorder.root) + + def test_finish_before_open_cleans_up_without_creating_an_attempt(self): + recorder = self.new(); report = recorder.finish('initialization_failed') + self.assertTrue(recorder.session.closed) + self.assertFalse(recorder.root.exists()) + self.assertEqual(report['status'], 'refused') + self.assertFalse(report['sealed']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_operator_export.py b/tests/test_operator_export.py new file mode 100644 index 0000000..affd04f --- /dev/null +++ b/tests/test_operator_export.py @@ -0,0 +1,130 @@ +"""Reviewed operator labels and causal feature history, using data fixtures.""" +from copy import deepcopy +import contextlib +import io +import json +from pathlib import Path +import unittest +from unittest.mock import patch + +import numpy as np + +from dvidia_training import camera_action_model as model +from dvidia_training import operator_capture as capture +from dvidia_training import operator_export as export +from tests.test_camera_action_capture import STUDENT, fake_estimate +from tests import test_operator_capture as fixtures + + +class OperatorExportTests(unittest.TestCase): + setUp = fixtures.OperatorCaptureTests.setUp + new = fixtures.OperatorCaptureTests.new + complete = fixtures.OperatorCaptureTests.complete + + def test_reviewed_labels_are_exact_issued_targets_and_keep_source_groups(self): + recorder, _, _ = self.complete() + dataset = export.export_episode(recorder.root, reviewed_indices=[0,2,5], + review_reason='Useful reach despite unsuccessful task.') + self.assertEqual(dataset['features'].shape, (3,269)) + np.testing.assert_array_equal(dataset['targets'], np.tile( + [*STUDENT['joint_targets_rad'], STUDENT['gripper_position_m']], (3,1))) + self.assertEqual(dataset['groups'], ['test-start']*3) + source = dataset['provenance']['episodes'][0] + self.assertEqual(source['reviewed_indices'], [0,2,5]) + self.assertFalse(source['attempt_strict_success']) + self.assertEqual(source['source_kind'], 'simulation_scripted_input') + self.assertEqual(source['label_source'], 'operator_issued') + self.assertFalse(source['review_authenticated']) + model._dataset(dataset, blind=False) + + def test_selection_uses_causal_full_prefix_history_before_subsetting(self): + recorder, _, _ = self.complete() + full = export.export_episode(recorder.root, reviewed_segments=[[0,10]], review_reason='Reviewed all rows.') + subset = export.export_episode(recorder.root, reviewed_segments=[[2,4],[7,9]], review_reason='Reviewed reaches.') + np.testing.assert_array_equal(subset['features'], full['features'][[2,3,7,8]]) + np.testing.assert_array_equal(subset['targets'], full['targets'][[2,3,7,8]]) + blind = export.export_episode(recorder.root, reviewed_indices=[3,7], review_reason='Reviewed.', blind=True) + np.testing.assert_array_equal(blind['features'][:,model.LIVE_FEATURE_START:],0) + + def test_review_selection_is_explicit_and_no_duplicates_or_bad_indices(self): + recorder, _, _ = self.complete() + for selection in ({}, {'reviewed_indices':[]}, {'reviewed_indices':[1,1]}, + {'reviewed_indices':[True]}, {'reviewed_indices':[10]}, + {'reviewed_segments':[[0,3],[2,4]]}, + {'reviewed_segments':[[0,2]],'reviewed_indices':[0]}): + with self.subTest(selection=selection), self.assertRaises(ValueError): + export.export_episode(recorder.root, review_reason='Reviewed.', **selection) + with self.assertRaisesRegex(ValueError, 'review reason'): + export.export_episode(recorder.root, reviewed_indices=[0]) + + def test_interrupted_complete_rows_require_review_but_remain_usable(self): + recorder = self.new(); recorder.open(); recorder.step(STUDENT); recorder.finish('operator_stopped') + dataset = export.export_episode(recorder.root, reviewed_indices=[0], review_reason='Retain useful first reach.') + self.assertEqual(dataset['provenance']['episodes'][0]['attempt_status'],'interrupted') + self.assertEqual(dataset['targets'].shape,(1,7)) + + def test_wrong_provenance_cannot_impersonate_operator_supervision(self): + recorder, _, _ = self.complete() + recording = capture.load_capture(recorder.root) + for change in ('teacher', 'student'): + modified = deepcopy(recording); modified['control_mode'] = change + with patch.object(capture, 'load_capture', return_value=modified): + with self.assertRaises(ValueError): + export.export_episode(recorder.root, reviewed_indices=[0], review_reason='Reviewed.') + + def test_unsealed_recovery_cannot_be_exported(self): + recorder = self.new(); recorder.open(); recorder.step(STUDENT) + recorder._journal.close(); recorder.session.close() + with self.assertRaises(FileNotFoundError): + export.export_episode(recorder.root, reviewed_indices=[0], review_reason='Reviewed.') + + def test_native_diagnostics_and_later_actions_do_not_leak_into_features(self): + recorder, _, _ = self.complete() + before = export.export_episode(recorder.root, reviewed_indices=[0,1,2], review_reason='Reviewed.') + other = self.new('other'); other.open() + for index in range(10): + action = deepcopy(STUDENT) + if index >= 3: + action['joint_targets_rad'][0] += .01 + other.step(action) + other.finish() + after = export.export_episode(other.root, reviewed_indices=[0,1,2], review_reason='Reviewed.') + np.testing.assert_array_equal(before['features'],after['features']) + np.testing.assert_array_equal(before['targets'],after['targets']) + + def test_dataset_roundtrip_and_exclusive_write(self): + recorder, _, _ = self.complete() + dataset = export.export_episode(recorder.root, reviewed_indices=[0,3,5], review_reason='Reviewed reaches.') + output = Path(self.temp.name)/'export' + self.assertEqual(export.save_dataset(dataset, output), output/'dataset.json') + loaded = export.load_dataset(output) + np.testing.assert_array_equal(loaded['features'], dataset['features']) + np.testing.assert_array_equal(loaded['targets'], dataset['targets']) + self.assertEqual(loaded['provenance'], dataset['provenance']) + with self.assertRaises(FileExistsError): + export.save_dataset(dataset, output) + target = output/'targets.npy' + body = target.read_bytes(); target.write_bytes(body[:-1]+bytes([body[-1]^1])) + with self.assertRaisesRegex(ValueError, 'byte identity'): + export.load_dataset(output) + + def test_cli_requires_exact_review_document_and_exports_without_training(self): + recorder, _, _ = self.complete() + review = Path(self.temp.name)/'review.json' + review.write_text(json.dumps(dict(segments=[[0,2]], reason='Reviewed initial reach.'))) + output = Path(self.temp.name)/'cli-export' + stdout = io.StringIO() + with contextlib.redirect_stdout(stdout): + self.assertEqual(export.main(['--capture',str(recorder.root), '--review',str(review), + '--output',str(output)]),0) + receipt = json.loads(stdout.getvalue()) + self.assertEqual(receipt['reviewed_rows'],2) + self.assertFalse(receipt['training_started']) + self.assertEqual(export.load_dataset(output)['features'].shape,(2,269)) + review.write_text(json.dumps(dict(indices=[0], reason='Reviewed.', extra=True))) + with contextlib.redirect_stderr(io.StringIO()), self.assertRaises(SystemExit): + export.main(['--capture',str(recorder.root), '--review',str(review), '--output',str(output)]) + + +if __name__ == '__main__': + unittest.main()