diff --git a/README.md b/README.md index c88821e..aa7694a 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,16 @@ [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) +**[Arm interfaces and installation v0.1](docs/arm-interfaces-v01.md).** +The **0.1.3.dev3 preview** adds `dvidia-arm`: three explicit native simulation +adapters, exact PiPER policy compatibility checks, separate install/run commands, +and a data-only lift-planning gate. A fixed tracking study retains **54/54 +numerically valid attempts**, including **21/1,260 offline compute-budget +exceedances**. These are mechanics checks, not task success or cross-arm transfer. +[Benchmark](benchmarks/arm-interfaces-v01/README.md) · +[Install the preview](https://github.com/Dvidia-Inference/dvidia-training/releases/tag/arm-interfaces-v0.1-preview) · +[Hugging Face evidence](https://huggingface.co/datasets/Dvidia/arm-interfaces-v01). + **[Build a reusable task environment](docs/task-environments-v02.md).** The **0.1.3.dev2 native preview** adds a local metric editor and `dvidia-environment`: multiple rigid objects, an open bin, explicit OpenUSD and diff --git a/benchmarks/arm-interfaces-v01/README.md b/benchmarks/arm-interfaces-v01/README.md new file mode 100644 index 0000000..a0d7baa --- /dev/null +++ b/benchmarks/arm-interfaces-v01/README.md @@ -0,0 +1,42 @@ +# DVIDIA arm interface tracking evidence v0.1 + +Three explicit native simulation interfaces: **PiPER**, **SO101**, and **UR5e + Robotiq 2F-85**. + +This is a local CPU mechanics and interface benchmark. It contains no trained policy, object manipulation task, physical robot connection or hardware-latency qualification. + +## What was run + +- 54 fixed attempts: three profiles × hold / bounded arm perturbation / gripper cycle × 10 / 20 / 40 ms updates × two repeats. +- Every attempt simulates 0.4 seconds at a native 1 ms timestep, without hidden settling or automatic retries. +- 54/54 attempts were numerically valid, with zero native warnings; this is not a task-success count. +- 21,654 recorded state rows and 1,260 issued command events are retained. +- **21 of 1,260 offline control blocks exceeded their nominal compute budget**: PiPER 13, SO101 1, UR5e/Robotiq 7. These are unpaced computation diagnostics, not measured physical deadlines. +- The independent read-only audit recomputed 648 RMS/max values with zero trace/source digest or command-alignment mismatches. + +All cases and repeats are retained. Repeats use identical deterministic initial states and trajectories for timing variability; they do not establish generalization, confidence intervals or statistically independent task trials. No profile or update period was selected automatically. + +## Accuracy and speed mean specific things + +Arm tracking is measured in radians. PiPER gripper tracking uses its single-finger slide command in metres, SO101 uses a moving-jaw hinge target in radians, and Robotiq uses its native 0–255 servo coordinate. These are reported separately. + +Robotiq feedback is derived from the pinned affine servo model as `-(bias0 + bias1 * actuator_length) / gain`. It is not an aperture, force or tactile measurement. Raw state and the mapping parameters are retained. + +Tracking errors compare observed state with both the held issued target and the continuous reference at the observation time. Terminal-link position/orientation errors use native forward kinematics at the desired arm configuration; the link origin is not a calibrated fingertip tool centre point. + +`compile_and_initialize_wall_s` includes pinned asset verification, model compilation and initialization. `control_loop_compute_s` includes the complete control block. **`native_step_compute_s` measures the full observed-step wrapper: native stepping, forward update, state checks/snapshot and reference FK. It is not pure physics-engine execution time.** Trace serialization is outside the loop; total attempt timing stops before writing the final result receipt. + +The run used Python 3.12.9, MuJoCo 3.15.0 and NumPy 2.5.3 on Darwin arm64. The machine was an interactive shared host, not an isolated real-time test rig. Upstream gravity compensation and mechanics differ by profile. Do not interpret this table as a hardware quality ranking, minimum-system specification or sensor-to-actuator latency measurement. + +## Read the evidence + +[Interface and installation guide](../../docs/arm-interfaces-v01.md) + +- [Bounded summary](summary.json): all 27 condition summaries, settings, source hashes and timing scope. +- [Independent audit](audit.json) and [original audit source](audit.py). +- [Prepared artifact identities](publication-artifacts.json): archive and full inventory hashes. + +The companion evidence ZIP retains every raw trace; large traces are excluded from Git. The portable verifier is inside the ZIP. This directory records prepared artifacts and does not itself claim a completed publication. + +Archive: `arm-interfaces-v01-release-20261031.zip` · 11,159,124 bytes · SHA-256 `48200eb5c647badff7db5ee14272571b93d9c31003d9f5f92dd6e28d062b272b`. + +Original DVIDIA source and synthetic records: MIT. PiPER simulation assets: MIT; SO101: Apache-2.0; UR5e: BSD-3-Clause; Robotiq 2F-85: BSD-2-Clause. Full upstream notices are preserved in the companion archive. diff --git a/benchmarks/arm-interfaces-v01/audit.json b/benchmarks/arm-interfaces-v01/audit.json new file mode 100644 index 0000000..d82dc61 --- /dev/null +++ b/benchmarks/arm-interfaces-v01/audit.json @@ -0,0 +1,12 @@ +{ + "schema": "dvidia.arm-interface-audit/v1", + "report_sha256": "5dd90299bd17515aee578631b46e3146c07ad18909eea838153d37bc4e9b1c5d", + "attempts": 54, + "state_rows": 21654, + "issued_command_events": 1260, + "independent_metric_comparisons": 648, + "source_and_trace_digest_mismatches": 0, + "command_time_alignment_mismatches": 0, + "offline_control_budget_exceedances": 21, + "scope": "Read-only numerical/digest audit; not a repeat run or physical validation." +} diff --git a/benchmarks/arm-interfaces-v01/audit.py b/benchmarks/arm-interfaces-v01/audit.py new file mode 100644 index 0000000..602308f --- /dev/null +++ b/benchmarks/arm-interfaces-v01/audit.py @@ -0,0 +1,71 @@ +"""Read-only independent audit of the fixed arm-interface tracking run.""" +from hashlib import sha256 +import json +import math +from pathlib import Path + +root=Path('runs/arm-interfaces-v01-20261031') +report=json.loads((root/'report.json').read_text()) +protocol_bytes=(root/'protocol.json').read_bytes() +assert sha256(protocol_bytes).hexdigest()==report['protocol']['sha256'] +protocol=json.loads(protocol_bytes) +assert len(protocol['schedule'])==54==len(report['attempts']) +assert all(sha256((root/'source'/f['name']).read_bytes()).hexdigest()==f['sha256'] for f in protocol['source_inventory']) +assert len({a['id'] for a in report['attempts']})==54 +rows_total=commands_total=metric_comparisons=0 +for scheduled,result in zip(protocol['schedule'],report['attempts']): + assert all(result[k]==v for k,v in scheduled.items()) + folder=root/result['id'] + assert json.loads((folder/'result.json').read_text())==result + assert json.loads((folder/'started.json').read_text())['attempt']==scheduled + raw=(folder/'trace.json').read_bytes() + assert sha256(raw).hexdigest()==result['trace']['sha256'] and len(raw)==result['trace']['bytes'] + trace=json.loads(raw) + rows,commands=trace['rows'],trace['commands'] + assert result['valid'] and not result['error'] and not result['warnings'] and not result['warning_counters'] + assert len(rows)==401 and len(commands)==round(.4/result['control_period_s']) + assert rows[-1]['elapsed_s']==.4 and result['simulated_seconds']==.4 + substeps=round(result['control_period_s']/.001) + arm_indices=result['contract']['arm_action_indices'] + gripper=result['gripper_coordinate'] + series={key:[] for key in result['metrics'] if key!='state_rows'} + for index,row in enumerate(rows): + assert math.isclose(row['elapsed_s'],index*.001,abs_tol=1e-12) + assert math.isclose(row['native_time_s'],row['elapsed_s'],abs_tol=1e-10) + if index==0: continue + held=commands[(index-1)//substeps]['values_native'] + assert held==row['applied_command_native'] + reference=row['continuous_reference_command_native'] + # Compiled interfaces have direct scalar arm joints at these qpos addresses. + actual=[row['qpos_native'][i] for i in arm_indices] + series['arm_held_target'].extend(value-held[i] for value,i in zip(actual,arm_indices)) + series['arm_continuous_reference'].extend(value-reference[i] for value,i in zip(actual,arm_indices)) + jaw=-(gripper['bias0']+gripper['bias1']*row['gripper_actuator_length_native'])/gripper['gain'] + series['gripper_held_target'].append(jaw-held[gripper['action_index']]) + series['gripper_continuous_reference'].append(jaw-reference[gripper['action_index']]) + xyz,goal=row['terminal_link_position_m'],row['reference_terminal_link_position_m'] + series['terminal_position_continuous_reference'].append(math.sqrt(sum((a-b)**2 for a,b in zip(xyz,goal)))) + quat,goalquat=row['terminal_link_quaternion_wxyz'],row['reference_terminal_link_quaternion_wxyz'] + dot=min(1.,max(0.,abs(sum(a*b for a,b in zip(quat,goalquat))))) + series['terminal_orientation_continuous_reference'].append(2*math.acos(dot)) + for key,values in series.items(): + measured=result['metrics'][key] + assert measured['count']==len(values) + rms=math.sqrt(sum(v*v for v in values)/len(values)) + assert math.isclose(measured['rms'],rms,abs_tol=1e-10,rel_tol=1e-10),(result['id'],key,measured['rms'],rms) + assert math.isclose(measured['max_abs'],max(map(abs,values)),abs_tol=1e-10,rel_tol=1e-10) + metric_comparisons+=2 + assert result['metrics']['arm_continuous_reference']['unit']=='rad' + assert result['metrics']['gripper_continuous_reference']['unit']==gripper['unit'] + timing=result['timing'] + assert len(timing['native_steps_s'])==400 and len(timing['control_intervals_s'])==len(commands) + assert timing['offline_control_budget_exceedances']==sum(v>result['control_period_s'] for v in timing['control_intervals_s']) + rows_total+=len(rows);commands_total+=len(commands) +identity=sha256((root/'report.json').read_bytes()).hexdigest() +output={'schema':'dvidia.arm-interface-audit/v1','report_sha256':identity,'attempts':54, + 'state_rows':rows_total,'issued_command_events':commands_total,'independent_metric_comparisons':metric_comparisons, + 'source_and_trace_digest_mismatches':0,'command_time_alignment_mismatches':0, + 'offline_control_budget_exceedances':sum(a['timing']['offline_control_budget_exceedances'] for a in report['attempts']), + 'scope':'Read-only numerical/digest audit; not a repeat run or physical validation.'} +Path('runs/arm-interfaces-v01-audit-20261031.json').write_text(json.dumps(output,indent=2)+'\n') +print(json.dumps(output,indent=2)) diff --git a/benchmarks/arm-interfaces-v01/installation-smoke.json b/benchmarks/arm-interfaces-v01/installation-smoke.json new file mode 100644 index 0000000..86d1271 --- /dev/null +++ b/benchmarks/arm-interfaces-v01/installation-smoke.json @@ -0,0 +1,2423 @@ +{ + "schema": "dvidia.arm-interface-installed-smoke/v1", + "classification": "installation_smoke_not_additional_benchmark", + "wheel": { + "filename": "dvidia_training-0.1.3.dev3-py3-none-any.whl", + "bytes": 5707568, + "sha256": "e8b03e4d718dde277d467f7fcca97b5540620fbf03b737b5a34ec3ff4b4ae8f9", + "built_from_source_inventory": "source-final-before.json", + "extra": "arm" + }, + "runtime": { + "PYTHONPATH_unset": true, + "checkout_not_on_sys_path": true, + "cwd_is_outside_checkout": true, + "distribution_version": "0.1.3.dev3", + "module_loaded_from_fresh_venv_site_packages": true, + "module_version": "0.1.3.dev3", + "mujoco": "3.15.0", + "numpy": "2.5.3", + "python": "3.12.9" + }, + "source_unchanged_after_commands": true, + "changed_source_paths": [], + "all_command_exits_expected": true, + "commands": [ + { + "id": "08-runtime-info", + "argv": [ + "python", + "-c", + "import sys,json,importlib.metadata,pathlib,dvidia_training,mujoco,numpy; p=pathlib.Path(dvidia_training.__file__).resolve(); print(json.dumps({'python':sys.version.split()[0],'distribution_version':importlib.metadata.version('dvidia-training'),'module_version':dvidia_training.__version__,'mujoco':mujoco.__version__,'numpy':numpy.__version__,'module_loaded_from_fresh_venv_site_packages':'site-packages' in p.parts and str(p).startswith(sys.prefix),'cwd_is_outside_checkout':'dvidia-arm-interfaces-20261031' not in str(pathlib.Path.cwd()),'PYTHONPATH_unset':__import__('os').environ.get('PYTHONPATH') is None,'checkout_not_on_sys_path':not any('dvidia-arm-interfaces-20261031' in str(x) for x in sys.path)},sort_keys=True))" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 15.071509167086333, + "stdout": "08-runtime-info.stdout.txt", + "stderr": "08-runtime-info.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "09-pip-check", + "argv": [ + "python", + "-m", + "pip", + "check" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 5.282357417047024, + "stdout": "09-pip-check.stdout.txt", + "stderr": "09-pip-check.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "10-list", + "argv": [ + "dvidia-arm", + "list" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 2.629831749945879, + "stdout": "10-list.stdout.txt", + "stderr": "10-list.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "11-describe-piper", + "argv": [ + "dvidia-arm", + "describe", + "piper" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 0.8112794170156121, + "stdout": "11-describe-piper.stdout.txt", + "stderr": "11-describe-piper.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "11-describe-so101", + "argv": [ + "dvidia-arm", + "describe", + "so101" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 0.5200761668384075, + "stdout": "11-describe-so101.stdout.txt", + "stderr": "11-describe-so101.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "11-describe-ur5e-2f85", + "argv": [ + "dvidia-arm", + "describe", + "ur5e-2f85" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 0.5499142920598388, + "stdout": "11-describe-ur5e-2f85.stdout.txt", + "stderr": "11-describe-ur5e-2f85.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "12-check-piper", + "argv": [ + "dvidia-arm", + "check", + "--model", + "model.json", + "--robot", + "piper" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 1.772932541090995, + "stdout": "12-check-piper.stdout.txt", + "stderr": "12-check-piper.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "13-reject-so101", + "argv": [ + "dvidia-arm", + "check", + "--model", + "model.json", + "--robot", + "so101" + ], + "exit_code": 2, + "expected_exit_codes": [ + 2 + ], + "exit_expected": true, + "wall_seconds": 0.8400740409269929, + "stdout": "13-reject-so101.stdout.txt", + "stderr": "13-reject-so101.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "14-reject-ur5e", + "argv": [ + "dvidia-arm", + "check", + "--model", + "model.json", + "--robot", + "ur5e-2f85" + ], + "exit_code": 2, + "expected_exit_codes": [ + 2 + ], + "exit_expected": true, + "wall_seconds": 1.2321779169142246, + "stdout": "14-reject-ur5e.stdout.txt", + "stderr": "14-reject-ur5e.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "15-reject-hardware", + "argv": [ + "dvidia-arm", + "check", + "--model", + "model.json", + "--robot", + "piper", + "--interface", + "piper-sdk-planned" + ], + "exit_code": 2, + "expected_exit_codes": [ + 2 + ], + "exit_expected": true, + "wall_seconds": 0.8657266246154904, + "stdout": "15-reject-hardware.stdout.txt", + "stderr": "15-reject-hardware.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "16-install", + "argv": [ + "dvidia-arm", + "install", + "--model", + "model.json", + "--robot", + "piper", + "--assets", + "robot-assets", + "--output", + "installation" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 9.095292792189866, + "stdout": "16-install.stdout.txt", + "stderr": "16-install.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "17-inspect", + "argv": [ + "dvidia-arm", + "inspect", + "installation", + "--assets", + "robot-assets" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 9.849826000165194, + "stdout": "17-inspect.stdout.txt", + "stderr": "17-inspect.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "18-run-selected-once", + "argv": [ + "dvidia-arm", + "run", + "installation", + "--assets", + "robot-assets", + "--start-x", + ".309", + "--start-y", + "-.04", + "--goal", + "1", + "--output", + "placement-attempt" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0, + 1 + ], + "exit_expected": true, + "wall_seconds": 24.44059733301401, + "stdout": "18-run-selected-once.stdout.txt", + "stderr": "18-run-selected-once.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "19-task-template", + "argv": [ + "dvidia-arm", + "task-template" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 1.374279208946973, + "stdout": "19-task-template.stdout.txt", + "stderr": "19-task-template.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + }, + { + "id": "20-assess-task", + "argv": [ + "dvidia-arm", + "assess-task", + "lift-conditions.json" + ], + "exit_code": 0, + "expected_exit_codes": [ + 0 + ], + "exit_expected": true, + "wall_seconds": 0.694312334060669, + "stdout": "20-assess-task.stdout.txt", + "stderr": "20-assess-task.stderr.txt", + "working_directory": "outside_source_checkout", + "PYTHONPATH": "unset" + } + ], + "selected_placement_attempts": 1, + "selected_start_xy_m": [ + 0.309, + -0.04 + ], + "selected_goal_index": 1, + "no_retry_or_retuning": true, + "model_sha256": "2976a605575d9edefb5db366e72c5e006c0ce85a5531ad3d61512436864c8ded", + "model_binding": { + "installation_sha256": "459b1ccc8aae0c586d718e44b03259f824fdd92331065b813d2898ee43e09638", + "model_sha256": "2976a605575d9edefb5db366e72c5e006c0ce85a5531ad3d61512436864c8ded", + "physical_robot_ready": false, + "run_model_matches_installation": true, + "schema": "dvidia.arm-skill-run-binding/v1" + }, + "installation_sha256": "459b1ccc8aae0c586d718e44b03259f824fdd92331065b813d2898ee43e09638", + "profile_static_contract_sha256": "c7d4bd0f28a3e19c6241072eb09ec8e1798356da82aa9396af1530417aaf69de", + "native_adapter_binding": { + "compiled_model_sha256": "e794fa258065f2bc91905926359f66cb998401e331dccc7cdd6bc10a36f27de5", + "mujoco_version": "3.15.0", + "numpy_version": "2.5.3", + "timestep_s": 0.001 + }, + "task_result": { + "success": true, + "native_valid": true, + "reason": "success", + "score": { + "success": true, + "reason": "success", + "reference_xy_m": [ + 0.4, + -0.08 + ], + "terminal_reference_success": true, + "native_outcome_success": true, + "native_outcome_valid": true, + "terminal_dwell_s": 4.719999999999999, + "required_dwell_s": 0.25, + "terminal_xy_error_m": 0.012002855712791811, + "trace_lift_dwell_s": 1.7599999999999998, + "required_lift_dwell_s": 0.1, + "sample_interval_s": 0.02, + "trajectory_complete": true, + "native_metrics_source": "simulator-authored episode record; not authenticated sensor input" + }, + "compiled_task_model_sha256": "291ed4d37c2e21af3dcb56587d081272d7fa0c2ddc13e1c85d5ccd6672913324", + "model_sha256": "2976a605575d9edefb5db366e72c5e006c0ce85a5531ad3d61512436864c8ded", + "attempt_wall_seconds": 19.9367199158296, + "timing_scope": "Local CPU simulation compute; excludes sensing, communications and hardware actuation.", + "episode_timing": { + "wall_seconds": 18.355436959303916, + "inference": { + "samples": 1000, + "p50_s": 0.0006275000050663948, + "p95_s": 0.0041410667123273, + "p99_s": 0.01161633645649999, + "maximum_s": 0.026329999789595604 + }, + "control_step": { + "samples": 1000, + "p50_s": 0.009501812979578972, + "p95_s": 0.03492438916582614, + "p99_s": 0.06734381252434103, + "maximum_s": 0.174085000064224 + }, + "missed_20ms_boundaries": 179, + "measurement_scope": "CPU compute only, excludes sensing, transport and real actuation", + "inference_samples_s": [ + 0.012932958081364632, + 0.000559207983314991, + 0.00039299996569752693, + 0.0003981669433414936, + 0.013104416895657778, + 0.0005522090941667557, + 0.0007227500900626183, + 0.00048495782539248466, + 0.0006874171085655689, + 0.0004608752205967903, + 0.0004124580882489681, + 0.00044562481343746185, + 0.00041716592386364937, + 0.0005268328823149204, + 0.000386958010494709, + 0.0013881251215934753, + 0.0004449170082807541, + 0.0005701249465346336, + 0.0006254580803215504, + 0.0004348750226199627, + 0.0006965417414903641, + 0.000517082866281271, + 0.00035958271473646164, + 0.0006047501228749752, + 0.0005593332462012768, + 0.0003938754089176655, + 0.00043066730722784996, + 0.0003725830465555191, + 0.0003898749127984047, + 0.0006184172816574574, + 0.0004580826498568058, + 0.0003443746827542782, + 0.0007866658270359039, + 0.00037787528708577156, + 0.0004069576971232891, + 0.00045549962669610977, + 0.001764541957527399, + 0.002042291685938835, + 0.000495667103677988, + 0.0013866671361029148, + 0.0004254588857293129, + 0.0014710831455886364, + 0.0012172502465546131, + 0.0008684997446835041, + 0.0005417917855083942, + 0.0003914171829819679, + 0.0006075422279536724, + 0.000624542124569416, + 0.0017117909155786037, + 0.0011522918939590454, + 0.0006229588761925697, + 0.0007464578375220299, + 0.0005514589138329029, + 0.0005646669305860996, + 0.0005274577997624874, + 0.0009971670806407928, + 0.0010976670309901237, + 0.0012186672538518906, + 0.0007495409809052944, + 0.0006165420636534691, + 0.0016918331384658813, + 0.002885083667933941, + 0.003930707927793264, + 0.00072825001552701, + 0.0017835418693721294, + 0.00044575007632374763, + 0.000897333025932312, + 0.0004420829936861992, + 0.0008007078431546688, + 0.0005458751693367958, + 0.00039058318361639977, + 0.0006081252358853817, + 0.0005209171213209629, + 0.0003675417974591255, + 0.00045854225754737854, + 0.0004829578101634979, + 0.0007611247710883617, + 0.00035233283415436745, + 0.0003947080112993717, + 0.00038787536323070526, + 0.0011247079819440842, + 0.0012748329900205135, + 0.0010222080163657665, + 0.0017678332515060902, + 0.005377875175327063, + 0.0011274171993136406, + 0.0007092091254889965, + 0.0014570839703083038, + 0.0016579581424593925, + 0.0009650830179452896, + 0.0006339163519442081, + 0.0006037498824298382, + 0.0004905411042273045, + 0.00048733409494161606, + 0.0004065842367708683, + 0.00048745889216661453, + 0.0004492918960750103, + 0.0031217499636113644, + 0.0003947499208152294, + 0.00042125023901462555, + 0.0007077907212078571, + 0.0009284159168601036, + 0.00043283402919769287, + 0.004139875061810017, + 0.005087084136903286, + 0.00045729102566838264, + 0.00045779114589095116, + 0.00044008297845721245, + 0.000587542075663805, + 0.0017134170047938824, + 0.0009530829265713692, + 0.001193000003695488, + 0.0021273749880492687, + 0.0005133328959345818, + 0.0009372918866574764, + 0.0010157921351492405, + 0.0014877496287226677, + 0.009719416964799166, + 0.0009990422986447811, + 0.0004333336837589741, + 0.004761500284075737, + 0.0005716658197343349, + 0.0015935827977955341, + 0.0005757496692240238, + 0.0005172081291675568, + 0.0006964169442653656, + 0.0009790421463549137, + 0.0003430410288274288, + 0.0004108748398721218, + 0.001154750119894743, + 0.0015245410613715649, + 0.0007973336614668369, + 0.000598750077188015, + 0.0017978749237954617, + 0.000961042009294033, + 0.0006289579905569553, + 0.0011908337473869324, + 0.0032436251640319824, + 0.0004665409214794636, + 0.0016702092252671719, + 0.003102458082139492, + 0.007031375076621771, + 0.0010794168338179588, + 0.0004954170435667038, + 0.0004887920804321766, + 0.0022940002381801605, + 0.00038516707718372345, + 0.00042616669088602066, + 0.0003759162500500679, + 0.0015834169462323189, + 0.0006061671301722527, + 0.0012658750638365746, + 0.0009205839596688747, + 0.0004100422374904156, + 0.0008429577574133873, + 0.0003881249576807022, + 0.0009831669740378857, + 0.0005146251060068607, + 0.0004038340412080288, + 0.0015441249124705791, + 0.0005414173938333988, + 0.0005353330634534359, + 0.0016104169189929962, + 0.0006087091751396656, + 0.0006749159656465054, + 0.00039954204112291336, + 0.0007917913608253002, + 0.0007217079401016235, + 0.023437374737113714, + 0.000478583388030529, + 0.0019009578973054886, + 0.0005444581620395184, + 0.0004483340308070183, + 0.0004205419681966305, + 0.0009619998745620251, + 0.00044737523421645164, + 0.0005329172126948833, + 0.0007558329962193966, + 0.0022334582172334194, + 0.0008480409160256386, + 0.0029472499154508114, + 0.0006602923385798931, + 0.0004770415835082531, + 0.00048179225996136665, + 0.0005665421485900879, + 0.0012369579635560513, + 0.00039133336395025253, + 0.001440166961401701, + 0.0007427078671753407, + 0.0010753339156508446, + 0.0007136669009923935, + 0.0006884592585265636, + 0.0014182496815919876, + 0.0005735419690608978, + 0.0014702500775456429, + 0.0007484578527510166, + 0.0005475827492773533, + 0.0006173746660351753, + 0.0005359998904168606, + 0.0005055000074207783, + 0.00039583398029208183, + 0.0009817089885473251, + 0.0011093751527369022, + 0.0009713335894048214, + 0.00044400012120604515, + 0.0004505421966314316, + 0.003928625024855137, + 0.0003886669874191284, + 0.000721542164683342, + 0.0011632908135652542, + 0.002118541859090328, + 0.0006925417110323906, + 0.0008982913568615913, + 0.0011555827222764492, + 0.0005507082678377628, + 0.0008695409633219242, + 0.0013530422002077103, + 0.0011012498289346695, + 0.0021733329631388187, + 0.00059487484395504, + 0.0013466249220073223, + 0.001477791927754879, + 0.003655749838799238, + 0.0010015829466283321, + 0.0029069590382277966, + 0.0004707910120487213, + 0.00151929073035717, + 0.0004332917742431164, + 0.000431000255048275, + 0.0003848751075565815, + 0.0004710000939667225, + 0.0011401250958442688, + 0.00038774963468313217, + 0.0005877497605979443, + 0.0006186249665915966, + 0.0010089580900967121, + 0.0006288331933319569, + 0.0007619578391313553, + 0.0006019170396029949, + 0.000939458142966032, + 0.0013420418836176395, + 0.00042733317241072655, + 0.00042075011879205704, + 0.0006936253048479557, + 0.0006987499073147774, + 0.002030957955867052, + 0.00048058293759822845, + 0.0004527079872786999, + 0.0005350410938262939, + 0.0012471252121031284, + 0.00125962495803833, + 0.0008166669867932796, + 0.001435333862900734, + 0.0012284996919333935, + 0.0004286249168217182, + 0.0007151667959988117, + 0.0007863747887313366, + 0.0006681252270936966, + 0.0003427919000387192, + 0.0005347919650375843, + 0.0003571249544620514, + 0.001302833203226328, + 0.0012014997191727161, + 0.002803791780024767, + 0.00039175013080239296, + 0.00048495782539248466, + 0.0003202911466360092, + 0.0007072496227920055, + 0.000391458161175251, + 0.0004730410873889923, + 0.00036624958738684654, + 0.0004032920114696026, + 0.0004086252301931381, + 0.0003686249256134033, + 0.0013510831631720066, + 0.0003525419160723686, + 0.0009226249530911446, + 0.001258790958672762, + 0.0006284578703343868, + 0.00038724998012185097, + 0.0009099170565605164, + 0.0007740841247141361, + 0.0006366251036524773, + 0.000589749775826931, + 0.0013520829379558563, + 0.0023207501508295536, + 0.0004492499865591526, + 0.0003865412436425686, + 0.0006052921526134014, + 0.0004843329079449177, + 0.00047333305701613426, + 0.000822416041046381, + 0.0008042906410992146, + 0.004611250013113022, + 0.0005019577220082283, + 0.0011979159899055958, + 0.0016568750143051147, + 0.0004474576562643051, + 0.0007865000516176224, + 0.000645957887172699, + 0.0004736669361591339, + 0.000380499754101038, + 0.0005859578959643841, + 0.0004960419610142708, + 0.00039229216054081917, + 0.0004809591919183731, + 0.0006315410137176514, + 0.00045900000259280205, + 0.00045233312994241714, + 0.00039449986070394516, + 0.0006140000186860561, + 0.00046224985271692276, + 0.000842374749481678, + 0.0004207910969853401, + 0.0003386246971786022, + 0.000858125276863575, + 0.0010919580236077309, + 0.0007008342072367668, + 0.0005227080546319485, + 0.0004980410449206829, + 0.0006586252711713314, + 0.000699915923178196, + 0.0006876667030155659, + 0.0005085412412881851, + 0.00045187491923570633, + 0.0008747498504817486, + 0.00041891634464263916, + 0.000995459035038948, + 0.004193041939288378, + 0.0004382082261145115, + 0.00047333305701613426, + 0.00033291708678007126, + 0.0009976248256862164, + 0.0012792912311851978, + 0.001098915934562683, + 0.0006902087479829788, + 0.00039029214531183243, + 0.0004543326795101166, + 0.0008290000259876251, + 0.0003698328509926796, + 0.0005277092568576336, + 0.0008550416678190231, + 0.020320374984294176, + 0.0003365832380950451, + 0.00045029167085886, + 0.00038291560485959053, + 0.0005021672695875168, + 0.00039616599678993225, + 0.0004799589514732361, + 0.0004336247220635414, + 0.0004610419273376465, + 0.0005343751981854439, + 0.0005667922087013721, + 0.000431958120316267, + 0.0004390827380120754, + 0.0011415421031415462, + 0.0015656249597668648, + 0.0003944588825106621, + 0.0004074997268617153, + 0.0005242503248155117, + 0.0004792497493326664, + 0.0007761670276522636, + 0.00043841684237122536, + 0.0004035411402583122, + 0.000379249919205904, + 0.003452249802649021, + 0.0004894169978797436, + 0.0008389586582779884, + 0.0010859998874366283, + 0.00045616598799824715, + 0.0005488749593496323, + 0.0006430000066757202, + 0.0004154588095843792, + 0.0012247497215867043, + 0.00036837486550211906, + 0.0004042922519147396, + 0.0006975838914513588, + 0.0006904168985784054, + 0.0005178330466151237, + 0.0004271250218153, + 0.0006460421718657017, + 0.0005466248840093613, + 0.00046337489038705826, + 0.00037729181349277496, + 0.0019327918998897076, + 0.0005200831219553947, + 0.0005353330634534359, + 0.00086162518709898, + 0.0004939157515764236, + 0.0006102081388235092, + 0.001802625134587288, + 0.00045133428648114204, + 0.0008037076331675053, + 0.002860375214368105, + 0.0010917922481894493, + 0.0005087079480290413, + 0.0023255827836692333, + 0.0003742501139640808, + 0.0003110002726316452, + 0.001107250340282917, + 0.0005066669546067715, + 0.0007888330146670341, + 0.00043783290311694145, + 0.000719917006790638, + 0.0004120003432035446, + 0.00044462503865361214, + 0.0014870422892272472, + 0.0021520829759538174, + 0.0004253750666975975, + 0.00044558290392160416, + 0.0011351252906024456, + 0.0007377914153039455, + 0.000582708977162838, + 0.0003992500714957714, + 0.0012902920134365559, + 0.0005813753232359886, + 0.000806999858468771, + 0.00039454177021980286, + 0.0007957499474287033, + 0.000991291832178831, + 0.00068612489849329, + 0.00046633416786789894, + 0.0011191247031092644, + 0.000809874851256609, + 0.0005470002070069313, + 0.0004041669890284538, + 0.0004094168543815613, + 0.0006837500259280205, + 0.0004521668888628483, + 0.0013472079299390316, + 0.0005714581348001957, + 0.000584624707698822, + 0.0004804586060345173, + 0.0007963343523442745, + 0.0007592919282615185, + 0.0005824589170515537, + 0.000719000119715929, + 0.0005874577909708023, + 0.001058707945048809, + 0.0006810827180743217, + 0.0005369582213461399, + 0.0004988326691091061, + 0.00043404102325439453, + 0.0005464581772685051, + 0.0005944170989096165, + 0.0004420410841703415, + 0.00033466611057519913, + 0.0022315415553748608, + 0.00039183301851153374, + 0.0005000000819563866, + 0.0004038331098854542, + 0.0017836671322584152, + 0.0004568747244775295, + 0.0005329158157110214, + 0.0004046661779284477, + 0.0004413747228682041, + 0.0005287081003189087, + 0.00068650022149086, + 0.00043404102325439453, + 0.0003443332388997078, + 0.00044620782136917114, + 0.0004158327355980873, + 0.000513582956045866, + 0.000383082777261734, + 0.0004093749448657036, + 0.0005948329344391823, + 0.0005432916805148125, + 0.0005660410970449448, + 0.0013864170759916306, + 0.0007715830579400063, + 0.0003848746418952942, + 0.0007742922753095627, + 0.0006196252070367336, + 0.0004900419153273106, + 0.0004917075857520103, + 0.0007521254010498524, + 0.0008186250925064087, + 0.0006999997422099113, + 0.0004499591886997223, + 0.0006666672416031361, + 0.001317082904279232, + 0.0004893750883638859, + 0.00045345816761255264, + 0.001154458150267601, + 0.004163708072155714, + 0.00039499998092651367, + 0.0005082087591290474, + 0.0008055828511714935, + 0.0012887082993984222, + 0.00040683289989829063, + 0.0007671252824366093, + 0.0006972500123083591, + 0.00039412500336766243, + 0.00043179094791412354, + 0.0006293337792158127, + 0.0006873328238725662, + 0.0006346246227622032, + 0.0006212093867361546, + 0.00041670817881822586, + 0.0005908752791583538, + 0.0005510831251740456, + 0.0005725831724703312, + 0.0005524996668100357, + 0.0005027907900512218, + 0.0012116669677197933, + 0.00037341704592108727, + 0.0009989580139517784, + 0.0010228343307971954, + 0.0005702921189367771, + 0.0010295417159795761, + 0.00047004222869873047, + 0.00042733317241072655, + 0.0004710420034825802, + 0.0004604160785675049, + 0.0004740827716886997, + 0.0008763750083744526, + 0.0004070829600095749, + 0.0010647079907357693, + 0.000636707991361618, + 0.0003508748486638069, + 0.001028707716614008, + 0.000457999762147665, + 0.0011341669596731663, + 0.00045566726475954056, + 0.0006344160065054893, + 0.0015051658265292645, + 0.0008518327958881855, + 0.0005083330906927586, + 0.0008292500860989094, + 0.00038904231041669846, + 0.0008402918465435505, + 0.001788834109902382, + 0.0005932501517236233, + 0.0003266250714659691, + 0.0005037500523030758, + 0.00050362478941679, + 0.0003305417485535145, + 0.000419416930526495, + 0.00042675016447901726, + 0.0004348750226199627, + 0.0008294167928397655, + 0.0003643753007054329, + 0.0008271252736449242, + 0.0011149169877171516, + 0.00046566687524318695, + 0.000367041677236557, + 0.0005689579993486404, + 0.001991833094507456, + 0.0006695417687296867, + 0.000588499940931797, + 0.0005213753320276737, + 0.0004524998366832733, + 0.0013584159314632416, + 0.001114583108574152, + 0.00039950013160705566, + 0.00036620907485485077, + 0.00038795825093984604, + 0.0003893752582371235, + 0.0005275420844554901, + 0.0008262079209089279, + 0.0011192499659955502, + 0.0009274161420762539, + 0.0007812078110873699, + 0.0010644160211086273, + 0.001021707896143198, + 0.0007473751902580261, + 0.0012535001151263714, + 0.00048737507313489914, + 0.0003976249136030674, + 0.0005619158037006855, + 0.0008619171567261219, + 0.002473750151693821, + 0.0005465000867843628, + 0.0005971658974885941, + 0.00047008320689201355, + 0.0005257907323539257, + 0.0007514171302318573, + 0.0006791246123611927, + 0.0007449169643223286, + 0.0004991251043975353, + 0.0006071249954402447, + 0.00035862531512975693, + 0.0003375420346856117, + 0.0005091670900583267, + 0.00037116697058081627, + 0.0008342503570020199, + 0.0008186246268451214, + 0.001651959028095007, + 0.0006137918680906296, + 0.0004587080329656601, + 0.00043345801532268524, + 0.000403708778321743, + 0.0003197910264134407, + 0.00044737476855516434, + 0.0003335829824209213, + 0.000581124797463417, + 0.00044391630217432976, + 0.0003527919761836529, + 0.0005390001460909843, + 0.0005190828815102577, + 0.0003980826586484909, + 0.00039350008592009544, + 0.0006881249137222767, + 0.00043616676703095436, + 0.0005211662501096725, + 0.00041949981823563576, + 0.0005516251549124718, + 0.0008760415948927402, + 0.00038566719740629196, + 0.000508124940097332, + 0.0003866669721901417, + 0.0005202502943575382, + 0.0004900419153273106, + 0.0007128752768039703, + 0.0004883748479187489, + 0.0004962091334164143, + 0.0009188749827444553, + 0.0004731249064207077, + 0.0005051670596003532, + 0.0006964998319745064, + 0.0005830833688378334, + 0.00040912535041570663, + 0.0007734172977507114, + 0.000554417259991169, + 0.0008820830844342709, + 0.0003775418736040592, + 0.0003475826233625412, + 0.0003865421749651432, + 0.0004922081716358662, + 0.000943250022828579, + 0.0005092499777674675, + 0.0004210001789033413, + 0.0004988326691091061, + 0.0005159592255949974, + 0.0004047499969601631, + 0.00032962486147880554, + 0.00033999979496002197, + 0.00038283318281173706, + 0.0004481659270823002, + 0.000392707996070385, + 0.000474375206977129, + 0.0005669579841196537, + 0.00044616591185331345, + 0.00031362520530819893, + 0.0009457501582801342, + 0.00039666611701250076, + 0.00034733396023511887, + 0.0003770408220589161, + 0.0007263747975230217, + 0.000382457859814167, + 0.0003908751532435417, + 0.0004470422863960266, + 0.0005286657251417637, + 0.0003392910584807396, + 0.0003926251083612442, + 0.0005314168520271778, + 0.0006183329969644547, + 0.00037966715171933174, + 0.0004920829087495804, + 0.00030674971640110016, + 0.00030125025659799576, + 0.00046779168769717216, + 0.00044787488877773285, + 0.0007640831172466278, + 0.0004548751749098301, + 0.00047079194337129593, + 0.0006034579128026962, + 0.0008148751221597195, + 0.0011192909441888332, + 0.00047191744670271873, + 0.0007423749193549156, + 0.0005369582213461399, + 0.0005291253328323364, + 0.0053964173421263695, + 0.0013406667858362198, + 0.0007258746773004532, + 0.0007816660217940807, + 0.0012772087939083576, + 0.0011335001327097416, + 0.000725042074918747, + 0.0006458340212702751, + 0.0021810419857501984, + 0.0005219588056206703, + 0.00041454099118709564, + 0.004955916665494442, + 0.0006137499585747719, + 0.00046395789831876755, + 0.00104554183781147, + 0.0006265421397984028, + 0.0015849173069000244, + 0.0005242922343313694, + 0.00048179086297750473, + 0.0004947497509419918, + 0.006385874934494495, + 0.0005902498960494995, + 0.002603875007480383, + 0.015864416025578976, + 0.0007730829529464245, + 0.0005759159103035927, + 0.0010250839404761791, + 0.001823625061661005, + 0.0033666659146547318, + 0.0006819581612944603, + 0.0005342504009604454, + 0.0004097083583474159, + 0.0006720414385199547, + 0.000632499810308218, + 0.0016128327697515488, + 0.00045966682955622673, + 0.001954833045601845, + 0.000707708764821291, + 0.0004371670074760914, + 0.0009332080371677876, + 0.0006407499313354492, + 0.001698208972811699, + 0.0025481670163571835, + 0.0005882079713046551, + 0.0006017093546688557, + 0.005877332761883736, + 0.0005131252110004425, + 0.0010735001415014267, + 0.00030583329498767853, + 0.0005982080474495888, + 0.026329999789595604, + 0.012734207790344954, + 0.014458124991506338, + 0.0006852499209344387, + 0.00044274982064962387, + 0.0005367500707507133, + 0.000430332962423563, + 0.0010626250877976418, + 0.00032966677099466324, + 0.0005004163831472397, + 0.0007002502679824829, + 0.0004185833968222141, + 0.0006519998423755169, + 0.0007031671702861786, + 0.000663041602820158, + 0.0003260839730501175, + 0.0006245002150535583, + 0.001104000024497509, + 0.000325583852827549, + 0.00045845797285437584, + 0.0006245002150535583, + 0.0011255410499870777, + 0.0004977919161319733, + 0.00043066730722784996, + 0.00044462503865361214, + 0.00046949973329901695, + 0.0005594170652329922, + 0.000795499887317419, + 0.0005365419201552868, + 0.00326820882037282, + 0.00048774993047118187, + 0.0006728330627083778, + 0.00046845898032188416, + 0.001994917169213295, + 0.001030375249683857, + 0.0036911661736667156, + 0.0038847499527037144, + 0.0011025420390069485, + 0.0003964169882237911, + 0.000937208067625761, + 0.0004038331098854542, + 0.000399791169911623, + 0.0006334171630442142, + 0.0008601658046245575, + 0.0010842499323189259, + 0.01203325018286705, + 0.0015976661816239357, + 0.00048733316361904144, + 0.0006786249577999115, + 0.001851916778832674, + 0.0005767922848463058, + 0.0009305421262979507, + 0.00401504198089242, + 0.00234412495046854, + 0.0019019166938960552, + 0.0038705840706825256, + 0.01161212520673871, + 0.007017125375568867, + 0.007720333989709616, + 0.0007095830515027046, + 0.002053250093013048, + 0.0028808340430259705, + 0.001201624982059002, + 0.007323583122342825, + 0.0017393752932548523, + 0.00047166692093014717, + 0.006568250246345997, + 0.0020782090723514557, + 0.0015414580702781677, + 0.008293415885418653, + 0.0005044997669756413, + 0.007901417091488838, + 0.00040791695937514305, + 0.0004432089626789093, + 0.0004549161531031132, + 0.002103374805301428, + 0.00039404118433594704, + 0.00047716638073325157, + 0.00040887482464313507, + 0.0005612079985439777, + 0.0010454170405864716, + 0.0010022078640758991, + 0.00266700005158782, + 0.0012846668250858784, + 0.006276416126638651, + 0.0006751250475645065, + 0.0014025000855326653, + 0.0012257080525159836, + 0.0008310000412166119, + 0.00446354178711772, + 0.0004957080818712711, + 0.0006744172424077988, + 0.0006258748471736908, + 0.0010411250405013561, + 0.002979292068630457, + 0.0025179581716656685, + 0.0014990000054240227, + 0.0015113339759409428, + 0.0005097081884741783, + 0.0009207911789417267, + 0.004861166700720787, + 0.0005288333632051945, + 0.0008146250620484352, + 0.006432915572077036, + 0.007320125121623278, + 0.00037908274680376053, + 0.000772333238273859, + 0.0007276670075953007, + 0.0019317921251058578, + 0.0038354997523128986, + 0.0004197079688310623, + 0.001975458115339279, + 0.0016396250575780869, + 0.003170040901750326, + 0.0005195420235395432, + 0.0008749170228838921, + 0.0005228747613728046, + 0.0006340001709759235, + 0.000530540943145752, + 0.0008195419795811176, + 0.0004887501709163189, + 0.0007908749394118786, + 0.0007910840213298798, + 0.00040495768189430237, + 0.0004900842905044556, + 0.00042333314195275307, + 0.0012463750317692757, + 0.0005314173176884651, + 0.0005065826699137688, + 0.0005305423401296139, + 0.0016624587588012218, + 0.0004226253367960453, + 0.0005443752743303776, + 0.0009127086959779263, + 0.0005054171197116375, + 0.0010730409994721413, + 0.005324458237737417, + 0.002512166742235422, + 0.0022202921099960804, + 0.0012783752754330635, + 0.0006360411643981934, + 0.0019877078011631966, + 0.0004475419409573078, + 0.000461542047560215, + 0.0015145828947424889, + 0.0004326659254729748, + 0.0006774170324206352, + 0.0012585828080773354, + 0.0007217498496174812, + 0.00041791703552007675, + 0.0012851669453084469, + 0.0011321250349283218, + 0.00047075003385543823, + 0.0027562500908970833, + 0.005479415878653526, + 0.0011205407790839672, + 0.0006625410169363022, + 0.003227540757507086, + 0.0009519997984170914, + 0.004264167044311762, + 0.004327917005866766, + 0.0005851248279213905, + 0.0004486655816435814, + 0.00047999992966651917, + 0.0007042083889245987, + 0.0004978328943252563, + 0.00042783329263329506, + 0.00042583420872688293, + 0.00036508310586214066, + 0.0004949998110532761, + 0.0006794161163270473, + 0.0007212911732494831, + 0.0006905421614646912, + 0.0003986665979027748, + 0.0019372501410543919, + 0.0006157080642879009, + 0.0004448341205716133, + 0.00037616677582263947, + 0.0007230839692056179, + 0.0038486248813569546, + 0.0011234590783715248, + 0.0015462501905858517, + 0.002058375161141157, + 0.000803832896053791, + 0.0011909590102732182, + 0.0017582080326974392, + 0.000814665574580431, + 0.002456833142787218, + 0.002346124965697527, + 0.0009259581565856934, + 0.0008891252800822258, + 0.0005642920732498169, + 0.00042300019413232803, + 0.0011249999515712261, + 0.0009190831333398819, + 0.0010042921639978886, + 0.0012484588660299778, + 0.003215082921087742, + 0.001272999681532383, + 0.00040399981662631035, + 0.00043737515807151794, + 0.0029417909681797028, + 0.0015795407816767693, + 0.001016582828015089, + 0.0015158751048147678, + 0.001020082738250494, + 0.0013343342579901218, + 0.001471749972552061, + 0.0009192498400807381, + 0.0024145422503352165, + 0.0018465830944478512, + 0.005339665804058313, + 0.0013277912512421608, + 0.0008772918954491615, + 0.0012619169428944588, + 0.008392790798097849, + 0.00047729117795825005, + 0.0005062082782387733, + 0.007353832945227623, + 0.00038333283737301826, + 0.00041687488555908203, + 0.0027430830523371696, + 0.0029803747311234474, + 0.0005089580081403255, + 0.0009187906980514526, + 0.0007338328287005424, + 0.0004282500594854355, + 0.009153666906058788, + 0.000528083648532629, + 0.0004729996435344219, + 0.008441750425845385, + 0.0022155828773975372, + 0.0006154580041766167, + 0.0006757918745279312, + 0.002106917090713978, + 0.00046104099601507187, + 0.004470250103622675, + 0.0018102079629898071, + 0.0019871247932314873, + 0.0010202080011367798, + 0.0008814581669867039, + 0.005603500176221132, + 0.0012647919356822968, + 0.0004774578846991062, + 0.0005320832133293152, + 0.0004172087647020817, + 0.0024204165674746037, + 0.003469333052635193, + 0.000896708108484745, + 0.0014316248707473278, + 0.0004542078822851181, + 0.008215541951358318, + 0.0024685407988727093, + 0.002037166152149439, + 0.0032461248338222504, + 0.009192333091050386, + 0.0019432082772254944, + 0.0012651672586798668, + 0.024590708781033754, + 0.00044191721826791763, + 0.0005805832333862782, + 0.001755999866873026, + 0.007919750176370144, + 0.00394766591489315, + 0.003409333061426878, + 0.0005776253528892994, + 0.0016667088493704796, + 0.001898791640996933, + 0.004981250036507845, + 0.0010501248762011528, + 0.005166708957403898, + 0.0008996245451271534, + 0.0025953752920031548, + 0.0005857911892235279, + 0.0021079578436911106, + 0.0012277080677449703, + 0.0017196666449308395 + ], + "control_step_samples_s": [ + 0.021645667031407356, + 0.008800291921943426, + 0.010022415779531002, + 0.014746082946658134, + 0.034971042070537806, + 0.009352041874080896, + 0.012072042096406221, + 0.014661124907433987, + 0.01001579174771905, + 0.011086040642112494, + 0.005746624898165464, + 0.005096124950796366, + 0.005193166434764862, + 0.0069265407510101795, + 0.00526933278888464, + 0.006947290617972612, + 0.007325541228055954, + 0.013679957948625088, + 0.004108167253434658, + 0.004080875311046839, + 0.006709333974868059, + 0.006411290727555752, + 0.008531041909009218, + 0.004849208984524012, + 0.004526750184595585, + 0.005223916843533516, + 0.005605707876384258, + 0.005278166849166155, + 0.013452166225761175, + 0.007845875341445208, + 0.0053555406630039215, + 0.004466000013053417, + 0.004760541021823883, + 0.004426458850502968, + 0.0064933751709759235, + 0.009803333319723606, + 0.009880874771624804, + 0.014038415625691414, + 0.00972354132682085, + 0.00760158384218812, + 0.0064085409976542, + 0.00660937512293458, + 0.011853749863803387, + 0.010452333837747574, + 0.007977625355124474, + 0.008593374863266945, + 0.005263166967779398, + 0.009172332938760519, + 0.01545270811766386, + 0.007998584304004908, + 0.007375332992523909, + 0.006776374764740467, + 0.009278874844312668, + 0.00520516699180007, + 0.008214792236685753, + 0.0060770828276872635, + 0.006204624660313129, + 0.009750833734869957, + 0.009347415994852781, + 0.016832999885082245, + 0.022447665687650442, + 0.01134291710332036, + 0.01593004073947668, + 0.0211343327537179, + 0.00678270822390914, + 0.006271041929721832, + 0.009040291886776686, + 0.009843207895755768, + 0.010029583238065243, + 0.00932470802217722, + 0.005685790907591581, + 0.01391708292067051, + 0.004641999956220388, + 0.011784708127379417, + 0.006638083141297102, + 0.01013204175978899, + 0.008494250010699034, + 0.006175416987389326, + 0.004483417142182589, + 0.00834624981507659, + 0.005113167222589254, + 0.021209499798715115, + 0.008988208137452602, + 0.012299208901822567, + 0.021630125120282173, + 0.012834874913096428, + 0.009278375189751387, + 0.012811333872377872, + 0.0071204169653356075, + 0.014476916752755642, + 0.010133791714906693, + 0.005634542088955641, + 0.005245875101536512, + 0.010216666851192713, + 0.005956916604191065, + 0.007718750275671482, + 0.008426249958574772, + 0.008918333798646927, + 0.0038685831241309643, + 0.009385125245898962, + 0.0192695832811296, + 0.018935374915599823, + 0.019062832929193974, + 0.03285654168576002, + 0.02682879101485014, + 0.01591675030067563, + 0.00836341641843319, + 0.017261791042983532, + 0.017818083055317402, + 0.019259625114500523, + 0.021542583126574755, + 0.013771708123385906, + 0.009126124903559685, + 0.016390083823353052, + 0.011515000369399786, + 0.020666665863245726, + 0.01820733305066824, + 0.017964167054742575, + 0.013098041992634535, + 0.008593958336859941, + 0.010077500250190496, + 0.018532083835452795, + 0.018113416619598866, + 0.026744416914880276, + 0.014200707897543907, + 0.004853458143770695, + 0.012826542370021343, + 0.004679833073168993, + 0.011120166163891554, + 0.00977958319708705, + 0.013340041041374207, + 0.013178749941289425, + 0.007096666377037764, + 0.012836874928325415, + 0.010663582943379879, + 0.005555540788918734, + 0.006603875197470188, + 0.010027334094047546, + 0.015276874881237745, + 0.0242402502335608, + 0.01625533401966095, + 0.020496916957199574, + 0.007584583014249802, + 0.007567124906927347, + 0.006363208871334791, + 0.011924332939088345, + 0.006211999803781509, + 0.006543207913637161, + 0.007012167014181614, + 0.011135291773825884, + 0.02051295805722475, + 0.014747582841664553, + 0.005646166857331991, + 0.016292499843984842, + 0.011857540812343359, + 0.007923041936010122, + 0.006520250346511602, + 0.006051499862223864, + 0.005061957985162735, + 0.008334875106811523, + 0.004774624947458506, + 0.019376832991838455, + 0.01604183344170451, + 0.004787416197359562, + 0.0070164999924600124, + 0.004862917121499777, + 0.00892695877701044, + 0.012881041038781404, + 0.028460917063057423, + 0.0067770411260426044, + 0.0067999581806361675, + 0.0076734996400773525, + 0.006255791988223791, + 0.006450458895415068, + 0.009255582932382822, + 0.005346791818737984, + 0.01121366722509265, + 0.007302375044673681, + 0.01258491724729538, + 0.011738082859665155, + 0.009188499767333269, + 0.012180165853351355, + 0.018112000077962875, + 0.005947500001639128, + 0.012845874764025211, + 0.008444415871053934, + 0.004313415847718716, + 0.01469466695562005, + 0.006432999856770039, + 0.012546499725431204, + 0.01141104195266962, + 0.008110290858894587, + 0.017023833002895117, + 0.012708458118140697, + 0.006908874958753586, + 0.008067042101174593, + 0.00930875027552247, + 0.007597082760185003, + 0.006839458830654621, + 0.011693958193063736, + 0.008437625132501125, + 0.011842374689877033, + 0.008420625235885382, + 0.012887583114206791, + 0.012594500090926886, + 0.005648499820381403, + 0.01039166608825326, + 0.007123458199203014, + 0.012642999645322561, + 0.022287957835942507, + 0.006193374749273062, + 0.012168416287750006, + 0.01119279209524393, + 0.022837416734546423, + 0.008181166835129261, + 0.00992974964901805, + 0.00637954194098711, + 0.010812874883413315, + 0.007977625355124474, + 0.011278124991804361, + 0.009248666930943727, + 0.021794292144477367, + 0.01626741699874401, + 0.0080891246907413, + 0.008237124886363745, + 0.010191457811743021, + 0.009465999901294708, + 0.01221116678789258, + 0.005774749908596277, + 0.007374749984592199, + 0.007547624874860048, + 0.009366084355860949, + 0.005073917098343372, + 0.013564332854002714, + 0.009368374943733215, + 0.007902707904577255, + 0.005473500117659569, + 0.005069625098258257, + 0.006138375028967857, + 0.005604875274002552, + 0.009886375162750483, + 0.011477333027869463, + 0.008219541981816292, + 0.011879290919750929, + 0.00795008335262537, + 0.015478791669011116, + 0.006546124815940857, + 0.010133458767086267, + 0.004442125093191862, + 0.016871584113687277, + 0.013272375334054232, + 0.005858459044247866, + 0.007003583014011383, + 0.01735295820981264, + 0.005058083217591047, + 0.0074231671169400215, + 0.0060395002365112305, + 0.0052126250229775906, + 0.005319999996572733, + 0.004114875104278326, + 0.010132291354238987, + 0.007963709067553282, + 0.010171541012823582, + 0.007503167260438204, + 0.004136582836508751, + 0.003993208985775709, + 0.005933415610343218, + 0.005398208741098642, + 0.004753999877721071, + 0.004901415668427944, + 0.004551915917545557, + 0.013721500057727098, + 0.009865375235676765, + 0.008553250227123499, + 0.005187375005334616, + 0.008602125104516745, + 0.008862792048603296, + 0.008633249904960394, + 0.004986042156815529, + 0.005201541818678379, + 0.009508125018328428, + 0.005290542263537645, + 0.00884945783764124, + 0.012522167060524225, + 0.01574608264490962, + 0.010087999980896711, + 0.01053199963644147, + 0.005794458091259003, + 0.006653625052422285, + 0.004729416687041521, + 0.009194291662424803, + 0.008254125248640776, + 0.008040083106607199, + 0.013240666128695011, + 0.014307915698736906, + 0.005136792082339525, + 0.0076342918910086155, + 0.0051537081599235535, + 0.005894250236451626, + 0.005543084349483252, + 0.005533166229724884, + 0.0049227080307900906, + 0.007151999976485968, + 0.004042292013764381, + 0.004926334135234356, + 0.00571800023317337, + 0.006911832839250565, + 0.008746333420276642, + 0.0049399579875171185, + 0.007160167209804058, + 0.009181040804833174, + 0.006036083213984966, + 0.015085124876350164, + 0.005680458154529333, + 0.004523833282291889, + 0.006264458876103163, + 0.023576084058731794, + 0.009833542164415121, + 0.007575499825179577, + 0.004433250054717064, + 0.004341125022619963, + 0.0076033747754991055, + 0.004974250216037035, + 0.005726207979023457, + 0.008048875257372856, + 0.0056503331288695335, + 0.00883449986577034, + 0.007503584027290344, + 0.01124458434060216, + 0.003864208236336708, + 0.012686708942055702, + 0.006487959064543247, + 0.006939374841749668, + 0.005903249606490135, + 0.0061571672558784485, + 0.007564374711364508, + 0.004059833008795977, + 0.008112917188555002, + 0.00528762536123395, + 0.005225916858762503, + 0.005521208979189396, + 0.007677415851503611, + 0.02557108411565423, + 0.005897374823689461, + 0.004376833327114582, + 0.005720625165849924, + 0.006660041399300098, + 0.0049390411004424095, + 0.011535624973475933, + 0.005769958253949881, + 0.00542808324098587, + 0.006219084374606609, + 0.005358832888305187, + 0.00554908299818635, + 0.007783709093928337, + 0.007979042362421751, + 0.008610624819993973, + 0.007191459182649851, + 0.0064811669290065765, + 0.00475516589358449, + 0.00453162519261241, + 0.008498959243297577, + 0.004735833033919334, + 0.010366832837462425, + 0.00453266641125083, + 0.011176667176187038, + 0.007496499922126532, + 0.014438708778470755, + 0.013074333313852549, + 0.005796249955892563, + 0.010210124775767326, + 0.007398415822535753, + 0.005232499912381172, + 0.0068269590847194195, + 0.006624958012253046, + 0.007716542109847069, + 0.00821016589179635, + 0.007875957991927862, + 0.006330166012048721, + 0.005722875241190195, + 0.005892917048186064, + 0.005080250091850758, + 0.00926487473770976, + 0.0057990411296486855, + 0.012109292205423117, + 0.01631908304989338, + 0.009071915876120329, + 0.008142584003508091, + 0.012273208238184452, + 0.009994999971240759, + 0.01072912523522973, + 0.008296709042042494, + 0.008963375352323055, + 0.01009312504902482, + 0.007750167045742273, + 0.012187249958515167, + 0.0062134587205946445, + 0.003469207789748907, + 0.005542541854083538, + 0.005865374580025673, + 0.01708058314397931, + 0.007956040557473898, + 0.008366917259991169, + 0.005176374688744545, + 0.011238290928304195, + 0.004732500296086073, + 0.009146083146333694, + 0.006536208093166351, + 0.004699582699686289, + 0.007211040705442429, + 0.00981195829808712, + 0.006794916931539774, + 0.0038056657649576664, + 0.006977708078920841, + 0.006930999923497438, + 0.007338874973356724, + 0.012805874925106764, + 0.006437249947339296, + 0.0075599998235702515, + 0.005390082951635122, + 0.00625312514603138, + 0.006573667284101248, + 0.007770957890897989, + 0.00490587530657649, + 0.005726292263716459, + 0.00453341705724597, + 0.00943854171782732, + 0.006952874828130007, + 0.011463083792477846, + 0.006978499703109264, + 0.011375084053725004, + 0.00557679170742631, + 0.005987292155623436, + 0.0076162912882864475, + 0.007527708075940609, + 0.014331500045955181, + 0.00922695780172944, + 0.005956791806966066, + 0.009210249874740839, + 0.007321707904338837, + 0.010495541151612997, + 0.00531962513923645, + 0.006224125158041716, + 0.005582249723374844, + 0.008845374919474125, + 0.0040999590419232845, + 0.005752332974225283, + 0.010233999695628881, + 0.006537583190947771, + 0.007101666182279587, + 0.006773417349904776, + 0.008944458793848753, + 0.004334708210080862, + 0.004812583792954683, + 0.00771070783957839, + 0.0043769581243395805, + 0.00940562505275011, + 0.006372916046530008, + 0.004249208141118288, + 0.0037117917090654373, + 0.007031083106994629, + 0.0056974999606609344, + 0.009812792297452688, + 0.004927207715809345, + 0.00399929191917181, + 0.009634291287511587, + 0.004895874764770269, + 0.00598933408036828, + 0.00718670804053545, + 0.01294795935973525, + 0.00401229178532958, + 0.014142250176519156, + 0.00696562509983778, + 0.0041841669008135796, + 0.008416916709393263, + 0.00966983288526535, + 0.008269833866506815, + 0.004307874944061041, + 0.007419792003929615, + 0.0062953331507742405, + 0.00577591685578227, + 0.006277041044086218, + 0.006158084142953157, + 0.00578287523239851, + 0.009241083636879921, + 0.005826124921441078, + 0.01173458294942975, + 0.013907957822084427, + 0.011023084167391062, + 0.009432291612029076, + 0.00927195930853486, + 0.006480082869529724, + 0.005081374663859606, + 0.006120416801422834, + 0.005958625115454197, + 0.009516875259578228, + 0.01656458294019103, + 0.008871333207935095, + 0.012032708153128624, + 0.011481875088065863, + 0.004964166786521673, + 0.0059826248325407505, + 0.00898616574704647, + 0.013039708137512207, + 0.01112812478095293, + 0.007117332890629768, + 0.005449333228170872, + 0.011557708028703928, + 0.007238291669636965, + 0.009561541024595499, + 0.004852707963436842, + 0.0050650411285459995, + 0.006885332986712456, + 0.005418249871581793, + 0.005416625179350376, + 0.007441624999046326, + 0.00603283429518342, + 0.008743708953261375, + 0.005323708988726139, + 0.006651749834418297, + 0.008651417214423418, + 0.009208792354911566, + 0.020958916749805212, + 0.0054292092099785805, + 0.005861000157892704, + 0.011667917016893625, + 0.014012875035405159, + 0.011514625046402216, + 0.015863416250795126, + 0.004535915795713663, + 0.0056020417250692844, + 0.013587499968707561, + 0.008142333012074232, + 0.005256957840174437, + 0.005827459041029215, + 0.00846887519583106, + 0.005167791619896889, + 0.00452004186809063, + 0.007038000039756298, + 0.004941374994814396, + 0.00556345796212554, + 0.009065999649465084, + 0.0058958749286830425, + 0.009091041050851345, + 0.009354165755212307, + 0.016135625075548887, + 0.00976904109120369, + 0.01668229093775153, + 0.004482958000153303, + 0.009210667107254267, + 0.00775216706097126, + 0.010654333047568798, + 0.011968000326305628, + 0.008311416022479534, + 0.022205207962542772, + 0.016705125104635954, + 0.020550415851175785, + 0.009110250044614077, + 0.012483750004321337, + 0.012787625193595886, + 0.03904595924541354, + 0.012787625193595886, + 0.013529832940548658, + 0.021885749883949757, + 0.012719084043055773, + 0.00678654108196497, + 0.02589729055762291, + 0.0216047503054142, + 0.016507042106240988, + 0.018514332827180624, + 0.0961549999192357, + 0.03571674972772598, + 0.026062125340104103, + 0.01274279085919261, + 0.013975000008940697, + 0.015500625129789114, + 0.013776334002614021, + 0.021086749620735645, + 0.028813582845032215, + 0.009431083220988512, + 0.0074044582433998585, + 0.006414708681404591, + 0.010340332984924316, + 0.012781040742993355, + 0.01133870892226696, + 0.00907529192045331, + 0.010305083356797695, + 0.009641374927014112, + 0.01017600018531084, + 0.007553250063210726, + 0.008664207998663187, + 0.005926541984081268, + 0.007178915664553642, + 0.005786625202745199, + 0.005830083042383194, + 0.007159708999097347, + 0.006859417073428631, + 0.00583241693675518, + 0.007450749631971121, + 0.007957375142723322, + 0.008458500262349844, + 0.00829512532800436, + 0.009968375321477652, + 0.008984207641333342, + 0.009915334172546864, + 0.008827208075672388, + 0.011075916700065136, + 0.009995208121836185, + 0.00837291730567813, + 0.009507833980023861, + 0.006725999992340803, + 0.009646750055253506, + 0.008731666021049023, + 0.012775667011737823, + 0.011181459296494722, + 0.009901207871735096, + 0.009025542065501213, + 0.010222082957625389, + 0.009889582637697458, + 0.014931957703083754, + 0.00990520790219307, + 0.006122790742665529, + 0.011925125028938055, + 0.006764791905879974, + 0.009123667143285275, + 0.005418417043983936, + 0.006979125086218119, + 0.008136166725307703, + 0.008192792069166899, + 0.007652374915778637, + 0.007671499624848366, + 0.008346166927367449, + 0.008268584031611681, + 0.010751666966825724, + 0.006690249778330326, + 0.007500541862100363, + 0.007313041016459465, + 0.007226083427667618, + 0.007069957908242941, + 0.007748750038444996, + 0.008363917004317045, + 0.008521166630089283, + 0.006378042045980692, + 0.007746874820441008, + 0.0065039582550525665, + 0.007787374779582024, + 0.008933417033404112, + 0.008358958177268505, + 0.01326095825061202, + 0.008217417169362307, + 0.007319915574043989, + 0.00807012477889657, + 0.006591207813471556, + 0.0069753751158714294, + 0.007319000083953142, + 0.008156041614711285, + 0.011398999951779842, + 0.010678208898752928, + 0.00647216709330678, + 0.005637375172227621, + 0.010261124931275845, + 0.008475166745483875, + 0.008445249870419502, + 0.009450457990169525, + 0.007957249879837036, + 0.014128583949059248, + 0.011158165987581015, + 0.010307999793440104, + 0.010033125057816505, + 0.010635499842464924, + 0.019946082960814238, + 0.016816167160868645, + 0.011139249894768, + 0.02161354199051857, + 0.02469437476247549, + 0.015527790877968073, + 0.02127504115924239, + 0.00891145784407854, + 0.030786124989390373, + 0.03346908278763294, + 0.027597833890467882, + 0.02469454240053892, + 0.03414887469261885, + 0.0247207498177886, + 0.027930291835218668, + 0.012527333106845617, + 0.031263959128409624, + 0.027771207969635725, + 0.025507166981697083, + 0.015611459035426378, + 0.015689207706600428, + 0.02001654217019677, + 0.04064270807430148, + 0.027540875133126974, + 0.0888857920654118, + 0.027382084168493748, + 0.05106083396822214, + 0.01678933436051011, + 0.019922791048884392, + 0.016710666939616203, + 0.02298379084095359, + 0.03606683388352394, + 0.016087458934634924, + 0.006382499821484089, + 0.016620292328298092, + 0.01554212486371398, + 0.019417874980717897, + 0.026193832978606224, + 0.0200142920948565, + 0.02184537472203374, + 0.012253582943230867, + 0.01120974961668253, + 0.017832790967077017, + 0.025187999941408634, + 0.008673624601215124, + 0.01677287509664893, + 0.026563957799226046, + 0.015232000034302473, + 0.02940770797431469, + 0.020089208148419857, + 0.039755832869559526, + 0.01594208274036646, + 0.08202379196882248, + 0.06204516626894474, + 0.06032179109752178, + 0.02922770893201232, + 0.004579833708703518, + 0.007034457754343748, + 0.006073916330933571, + 0.016470040660351515, + 0.03145879087969661, + 0.0064264582470059395, + 0.005774333141744137, + 0.007747708819806576, + 0.009480667300522327, + 0.00583479180932045, + 0.004487250000238419, + 0.004988374654203653, + 0.007615457754582167, + 0.006871458142995834, + 0.009722999762743711, + 0.005689083132892847, + 0.012620542198419571, + 0.0111133330501616, + 0.005843167193233967, + 0.015345667023211718, + 0.007188125047832727, + 0.015700291842222214, + 0.016072583384811878, + 0.009426082950085402, + 0.00627245893701911, + 0.00530204176902771, + 0.010768167208880186, + 0.010880791116505861, + 0.018985250033438206, + 0.02095391694456339, + 0.017266583163291216, + 0.019435999915003777, + 0.020446457900106907, + 0.03732383297756314, + 0.007867417298257351, + 0.01608708268031478, + 0.010128666181117296, + 0.012567458674311638, + 0.016455040778964758, + 0.009163707960397005, + 0.01820358308032155, + 0.023545041680336, + 0.031869125086814165, + 0.10361279174685478, + 0.015339209232479334, + 0.033112416975200176, + 0.03874487476423383, + 0.030067916959524155, + 0.03179433289915323, + 0.04820433259010315, + 0.016659957822412252, + 0.01357737509533763, + 0.04926529200747609, + 0.11152358306571841, + 0.045759459026157856, + 0.02218387508764863, + 0.174085000064224, + 0.06958158407360315, + 0.0219505843706429, + 0.04856895795091987, + 0.04691908322274685, + 0.007350333034992218, + 0.013605833984911442, + 0.04540454130619764, + 0.07276250002905726, + 0.018749292008578777, + 0.02399850031360984, + 0.0053937919437885284, + 0.033879249822348356, + 0.006596209015697241, + 0.022084166295826435, + 0.006570457946509123, + 0.016896999906748533, + 0.012072250247001648, + 0.010582583025097847, + 0.011038000229746103, + 0.011516749858856201, + 0.007559624966233969, + 0.014562875032424927, + 0.0157497082836926, + 0.0264075412414968, + 0.01840500021353364, + 0.013540292158722878, + 0.10147070838138461, + 0.032065125182271004, + 0.0065887500531971455, + 0.008749375119805336, + 0.012309374753385782, + 0.01268424978479743, + 0.036939000245183706, + 0.015302082989364862, + 0.01693974994122982, + 0.011585792060941458, + 0.01517570810392499, + 0.010031125042587519, + 0.0216855825856328, + 0.03433987498283386, + 0.03492208290845156, + 0.026009915862232447, + 0.040677625220268965, + 0.0673212087713182, + 0.019655124749988317, + 0.009036042261868715, + 0.008278249762952328, + 0.019329375121742487, + 0.020508707966655493, + 0.035549874883145094, + 0.013333083130419254, + 0.014233292080461979, + 0.008329249918460846, + 0.016168667003512383, + 0.022314957808703184, + 0.023358208127319813, + 0.016809542197734118, + 0.013665332924574614, + 0.009495791979134083, + 0.00808349996805191, + 0.005264958366751671, + 0.009421333204954863, + 0.008470333181321621, + 0.008342375047504902, + 0.004857291933149099, + 0.0055182920768857, + 0.00898066721856594, + 0.005413500126451254, + 0.009760749991983175, + 0.007971166167408228, + 0.013127000071108341, + 0.012072124984115362, + 0.005915999878197908, + 0.009547250345349312, + 0.009190250188112259, + 0.04164104210212827, + 0.043036459013819695, + 0.0111498748883605, + 0.011561125051230192, + 0.010462458245456219, + 0.006142999976873398, + 0.010509624611586332, + 0.014747417066246271, + 0.02447333373129368, + 0.009059166070073843, + 0.015092541929334402, + 0.0178394578397274, + 0.012042125221341848, + 0.009709457866847515, + 0.02187200030311942, + 0.022692583967000246, + 0.018207792192697525, + 0.026200250256806612, + 0.012618250213563442, + 0.031805625185370445, + 0.028459458146244287, + 0.017385500017553568, + 0.012647583149373531, + 0.02065875008702278, + 0.017396291717886925, + 0.016894500236958265, + 0.01258204085752368, + 0.02413887483999133, + 0.012556707952171564, + 0.019373625051230192, + 0.013555292040109634, + 0.004554165992885828, + 0.01123795798048377, + 0.00546662462875247, + 0.009547833353281021, + 0.01739745819941163, + 0.00835962500423193, + 0.006517915986478329, + 0.011996124871075153, + 0.027034624945372343, + 0.004809875041246414, + 0.00577616598457098, + 0.00453687459230423, + 0.005474666133522987, + 0.014558083843439817, + 0.034026334062218666, + 0.018945374991744757, + 0.011466291267424822, + 0.017496082931756973, + 0.019088999833911657, + 0.00872049992904067, + 0.007824541069567204, + 0.02061345800757408, + 0.013063124846667051, + 0.011276999954134226, + 0.005797583144158125, + 0.008747291751205921, + 0.005722374655306339, + 0.013822291977703571, + 0.016866709105670452, + 0.023215833120048046, + 0.04255437524989247, + 0.02583341719582677, + 0.039391166996210814, + 0.0058556669391691685, + 0.007237333804368973, + 0.02239829208701849, + 0.030780999921262264, + 0.023175000213086605, + 0.034360541962087154, + 0.02512266719713807, + 0.02242066664621234, + 0.017990250140428543, + 0.025538957677781582, + 0.01033874973654747, + 0.025263415649533272, + 0.050506291911005974, + 0.01876216661185026, + 0.030372209381312132, + 0.043630999978631735, + 0.03339595813304186, + 0.028272457886487246, + 0.039410832803696394, + 0.033771750051528215, + 0.010008458979427814, + 0.01263887481763959, + 0.03366787498816848, + 0.05415695812553167, + 0.030155541840940714, + 0.05252495873719454, + 0.024997999891638756, + 0.025016165804117918, + 0.03717141691595316, + 0.03436983283609152, + 0.04243804095312953, + 0.04409520793706179, + 0.026871500071138144, + 0.030032832641154528, + 0.02464712504297495, + 0.024628084152936935, + 0.023673875257372856, + 0.034631624817848206, + 0.031070291996002197, + 0.01678466610610485, + 0.012164582964032888, + 0.015401915647089481, + 0.03605412505567074, + 0.015642957761883736, + 0.014275957830250263, + 0.009054082911461592, + 0.025095541961491108, + 0.032826416194438934, + 0.07475033309310675, + 0.015457542147487402, + 0.04209970775991678, + 0.00925858411937952, + 0.03020104207098484, + 0.025685790926218033, + 0.021710500121116638, + 0.03378866706043482, + 0.03032379224896431, + 0.03243133379146457, + 0.015653500333428383, + 0.03359175007790327, + 0.008347666822373867, + 0.024942167103290558, + 0.024593625217676163, + 0.03202591696754098, + 0.03496820805594325, + 0.045455874875187874, + 0.03498570900410414, + 0.01344624999910593, + 0.042080583050847054, + 0.010987875051796436, + 0.021931250113993883, + 0.02948775002732873, + 0.03127441694959998, + 0.018714083824306726, + 0.02984629198908806, + 0.03805937524884939, + 0.017638332676142454, + 0.023371999617666006 + ], + "process_peak_rss_bytes": 578879488 + }, + "episode_simulated_seconds": 20.00000000000146, + "episode_trajectory_rows": 1001, + "speed_summary": { + "simulated_seconds": 20.00000000000146, + "rollout_compute_wall_seconds": 18.355436959303916, + "simulated_seconds_per_rollout_wall_second": 1.0895954176598317, + "inference_p95_s": 0.0041410667123273, + "control_step_p95_s": 0.03492438916582614, + "missed_nominal_20ms_compute_boundaries": 179, + "control_boundaries": 1000, + "interpretation": "Unpaced CPU compute in this one installation smoke on the current machine; 179/1000 nominal 20ms compute budgets exceeded. Not a hard-real-time, sensor or hardware latency result." + } + }, + "task_assessment_status": "test_lift_required", + "physical_robot_ready": false, + "fresh_model_download": false, + "fresh_asset_fetch": false, + "dependency_install": "Fresh final venv from existing cached official wheels; no index.", + "scope": "Single selected local native installation smoke, not a held-out evaluation or hardware test. Prior frozen 5/12 learned held-out benchmark is unchanged. Speed includes local simulation compute only, not sensing, bus or physical actuation." +} diff --git a/benchmarks/arm-interfaces-v01/publication-artifacts.json b/benchmarks/arm-interfaces-v01/publication-artifacts.json new file mode 100644 index 0000000..6f85c73 --- /dev/null +++ b/benchmarks/arm-interfaces-v01/publication-artifacts.json @@ -0,0 +1,36 @@ +{ + "schema": "dvidia.arm-interface-publication-artifacts/v1", + "archive": { + "filename": "arm-interfaces-v01-release-20261031.zip", + "bytes": 11159124, + "sha256": "48200eb5c647badff7db5ee14272571b93d9c31003d9f5f92dd6e28d062b272b" + }, + "inventory": { + "path": "manifest.json", + "bytes": 33313, + "sha256": "a567728776b3f6d50fe3f15df422de363e952f52ff814d6b8e4abdab08227632", + "file_count": 187, + "total_payload_bytes": 50924022 + }, + "scientific_report_sha256": "5dd90299bd17515aee578631b46e3146c07ad18909eea838153d37bc4e9b1c5d", + "audit": { + "schema": "dvidia.arm-interface-audit/v1", + "report_sha256": "5dd90299bd17515aee578631b46e3146c07ad18909eea838153d37bc4e9b1c5d", + "attempts": 54, + "state_rows": 21654, + "issued_command_events": 1260, + "independent_metric_comparisons": 648, + "source_and_trace_digest_mismatches": 0, + "command_time_alignment_mismatches": 0, + "offline_control_budget_exceedances": 21, + "scope": "Read-only numerical/digest audit; not a repeat run or physical validation." + }, + "sanitization": { + "schema": "dvidia.arm-interface-sanitization/v1", + "text_files_scanned": 186, + "personal_path_findings": 0, + "credential_pattern_findings": 0, + "scope": "UTF-8 artifact scan for user-specific absolute paths, common provider token forms, private-key blocks, bearer credentials and credential-bearing URLs. Public SHA-256 values and upstream source URLs are intentional." + }, + "scope": "Prepared local evidence only; not uploaded or published. Git commit intentionally omitted until recorded." +} diff --git a/benchmarks/arm-interfaces-v01/summary.json b/benchmarks/arm-interfaces-v01/summary.json new file mode 100644 index 0000000..ddb497a --- /dev/null +++ b/benchmarks/arm-interfaces-v01/summary.json @@ -0,0 +1,1263 @@ +{ + "schema": "dvidia.arm-interface-benchmark-summary/v1", + "scope": "Unloaded native CPU tracking of authored setpoints using pinned upstream mechanics. Privileged simulator state; unpaced offline computation. No object task, learned policy, hardware latency, calibrated jaw gap/force or physical robot qualification.", + "settings": { + "accuracy": "Every post-step native millisecond, against both the applied held target and continuous reference at observation time.", + "arm_amplitude_limit_rad": 0.04, + "cases": [ + "hold", + "arm_perturbation", + "gripper_cycle" + ], + "command_sampling": "At interval start; held until the next control boundary. No terminal command after the horizon.", + "control_periods_s": [ + 0.01, + 0.02, + 0.04 + ], + "duration_s": 0.4, + "gripper_cycle_fraction_of_range": 0.2, + "pacing": "Unpaced offline; budget exceedances compare computation with nominal control periods only.", + "physics_timestep_s": 0.001, + "profiles": [ + "piper", + "so101", + "ur5e-2f85" + ], + "repeats": 2, + "waveforms": { + "arm": "home + signed amplitude*sin(2*pi*t/duration)", + "gripper": "home + amplitude*sin(pi*t/duration)^2" + } + }, + "platform": { + "machine": "arm64", + "python": "3.12.9", + "system": "Darwin" + }, + "source_inventory": [ + { + "bytes": 24648, + "name": "arm_interface_benchmark.py", + "sha256": "2b70e624264bed0c003d4836fee192f9b16d22dc59a91dd9dd54180abf4ed857" + }, + { + "bytes": 19021, + "name": "arm_interfaces.py", + "sha256": "f5fd3952b194bc7d12dfefad25aa4bb6057a1bade90d593242b2c5099fb3570d" + }, + { + "bytes": 9837, + "name": "asset_library.py", + "sha256": "347bfdeb61e22eb3f65e317c18f94d3b2076d05def77630ebbb6e7dde427c2a7" + }, + { + "bytes": 37117, + "name": "asset_catalog.json", + "sha256": "592f363389a96fef7f9223737aa72a2f1a74f400238924bf6790ec5bfd3302b7" + }, + { + "bytes": 24336, + "name": "reference_sim.py", + "sha256": "c1201d22eb13dd07a236ee63cc4ba68e129f6067e8db61ed41af2d820eb5e3b8" + }, + { + "bytes": 10029, + "name": "reference_assets.lock.json", + "sha256": "941eb6f9fd474635a590b82c66b59a9d5124d8e0d9ddc203006f5b921d8d2861" + }, + { + "bytes": 26493, + "name": "robot_profiles.py", + "sha256": "fb90782a6a9979e282182118ae0e3e29d00f101255cc57378dd3f692672bb13d" + }, + { + "bytes": 35002, + "name": "robot_profiles.lock.json", + "sha256": "4b2ee2ca675aceb2f826983cb3082a3e08e9aa0ff419b23078abcff08ebcc6fe" + }, + { + "bytes": 26684, + "name": "env.py", + "sha256": "929c1d8c3c4cafe0054c633ee5f3e51c551dd6b26ae014ab1ea9219b58eca8e4" + } + ], + "protocol": { + "bytes": 10869, + "sha256": "30297af4696891b831332690eba4f0971f31c2d22ff453cfb52fbf2c2ddddabb" + }, + "scientific_report_sha256": "5dd90299bd17515aee578631b46e3146c07ad18909eea838153d37bc4e9b1c5d", + "summary": { + "attempts": 54, + "failed_attempts": 0, + "groups": [ + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 80, + "max": 0.08617058303207159, + "mean": 0.0066606729524210095, + "min": 0.0015232092700898647, + "p50": 0.0047638751566410065, + "p95": 0.014799892110750078, + "p99": 0.03052239155396775 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 1.072035066032009e-18, + "rms": 2.6891614825055087e-19, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.62491936055005e-22, + "rms": 1.7447726434640227e-22, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 12, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 40, + "max": 0.01233124965801835, + "mean": 0.0038010406540706755, + "min": 0.0018646251410245895, + "p50": 0.0031147501431405544, + "p95": 0.0071542119141668045, + "p99": 0.010771038737148044 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 1.072035066032009e-18, + "rms": 2.6891614825055087e-19, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.62491936055005e-22, + "rms": 1.7447726434640227e-22, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 20, + "max": 0.017511708196252584, + "mean": 0.004822466848418116, + "min": 0.0022527091205120087, + "p50": 0.00392885459586978, + "p95": 0.008204558771103628, + "p99": 0.01565027831122278 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 1.072035066032009e-18, + "rms": 2.6891614825055087e-19, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.62491936055005e-22, + "rms": 1.7447726434640227e-22, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0, + "rms": 0.0, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 80, + "max": 0.009031584020704031, + "mean": 0.0016621474002022295, + "min": 0.0008142921142280102, + "p50": 0.0014092291239649057, + "p95": 0.0029117225902155017, + "p99": 0.00530670105945316 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.04724642502768296, + "rms": 0.0259189106384304, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.0001351994203390188, + "rms": 5.080094981729882e-05, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.10438561600680343, + "rms": 0.06731460790372004, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.021219005288598238, + "rms": 0.012632019707729416, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 40, + "max": 0.05367350019514561, + "mean": 0.00433168753515929, + "min": 0.001299167051911354, + "p50": 0.0026975630316883326, + "p95": 0.008022505533881482, + "p99": 0.036394225121475735 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.049326113426734604, + "rms": 0.0266725827200244, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.00012803344853616733, + "rms": 4.849687059039438e-05, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.10717565871593182, + "rms": 0.06881010138097117, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.022190858533605844, + "rms": 0.013150654546350329, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 1, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 20, + "max": 0.004087208304554224, + "mean": 0.002820685459300876, + "min": 0.002134665846824646, + "p50": 0.0025993750896304846, + "p95": 0.0037898978451266887, + "p99": 0.004027746212668716 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.052438830751684054, + "rms": 0.0281278712769055, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.00010476945939613698, + "rms": 4.452074267440635e-05, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.11252260489735348, + "rms": 0.0715935747506626, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.02376565718713276, + "rms": 0.014051373013914846, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 80, + "max": 0.004917166195809841, + "mean": 0.0012033292092382907, + "min": 0.0007904996164143085, + "p50": 0.0009583958890289068, + "p95": 0.0021210480947047466, + "p99": 0.003010369786061331 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 2.065458367289029e-06, + "rms": 5.253991747278222e-07, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.004242146705405987, + "rms": 0.0027446238272243783, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 2.073779136790703e-06, + "rms": 1.2908457178922924e-06, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 7.645892876832497e-07, + "rms": 4.757995861420835e-07, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 40, + "max": 0.005131791345775127, + "mean": 0.0018414500751532613, + "min": 0.0012515839189291, + "p50": 0.0014223128091543913, + "p95": 0.003967785486020145, + "p99": 0.005110650369897485 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 2.0616016482556743e-06, + "rms": 5.156666908733432e-07, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.00439790975635305, + "rms": 0.002841191176530075, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 2.0699209397061008e-06, + "rms": 1.2668975659800765e-06, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 7.6316225784588e-07, + "rms": 4.6698384062746096e-07, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 20, + "max": 0.006313000340014696, + "mean": 0.003634768771007657, + "min": 0.002731082960963249, + "p50": 0.0033808329608291388, + "p95": 0.0053234161809086805, + "p99": 0.006115083508193491 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 2.017621775088743e-06, + "rms": 4.940004529333514e-07, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.004704818823770997, + "rms": 0.003022171434978861, + "unit": "m" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 2.026119605515511e-06, + "rms": 1.2137267925339343e-06, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 7.468847515545881e-07, + "rms": 4.4735832857372413e-07, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "piper", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 80, + "max": 0.007386083249002695, + "mean": 0.0018362766189966352, + "min": 0.0006946250796318054, + "p50": 0.0013957498595118523, + "p95": 0.005042399582453071, + "p99": 0.006279161768034092 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008273125449575409, + "rms": 0.00033999962887419643, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.5501089682247907e-06, + "rms": 3.3623058696310616e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016510537681685671, + "rms": 0.0011852927877226925, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003429001754549268, + "rms": 0.00024116740126096947, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 40, + "max": 0.00399504229426384, + "mean": 0.0023015928687527775, + "min": 0.001329083926975727, + "p50": 0.0023684795014560223, + "p95": 0.003282639337703585, + "p99": 0.0037617734819650646 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008273125449575409, + "rms": 0.00033999962887419643, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.5501089682247907e-06, + "rms": 3.3623058696310616e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016510537681685671, + "rms": 0.0011852927877226925, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003429001754549268, + "rms": 0.00024116740126096947, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 20, + "max": 0.007814249955117702, + "mean": 0.0045943541917949915, + "min": 0.002784957643598318, + "p50": 0.004228687146678567, + "p95": 0.0071518624434247615, + "p99": 0.007681772452779114 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008273125449575409, + "rms": 0.00033999962887419643, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 3.5501089682247907e-06, + "rms": 3.3623058696310616e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016510537681685671, + "rms": 0.0011852927877226925, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003429001754549268, + "rms": 0.00024116740126096947, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 80, + "max": 0.03294887486845255, + "mean": 0.0023548394965473562, + "min": 0.0007292916998267174, + "p50": 0.00166637497022748, + "p95": 0.00406429162248969, + "p99": 0.013818037696182575 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.009648770514443216, + "rms": 0.0037458833325678793, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 4.7884265412057665e-06, + "rms": 3.6620513790191328e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.016190626819596828, + "rms": 0.006736781120174303, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.002897765942538507, + "rms": 0.0011735273435602572, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 1, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 40, + "max": 0.004444666672497988, + "mean": 0.0017994416644796728, + "min": 0.0010527078993618488, + "p50": 0.0016369579825550318, + "p95": 0.0034136807313188905, + "p99": 0.004105366813018918 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.015516116990219667, + "rms": 0.006685054758093967, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 5.427756228551698e-06, + "rms": 4.097935989804088e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.026446557471206875, + "rms": 0.011817766498952506, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.004609791604128084, + "rms": 0.00201894266998173, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 20, + "max": 0.009385792072862387, + "mean": 0.004458368662744761, + "min": 0.0027221660129725933, + "p50": 0.004178020637482405, + "p95": 0.008225643681362272, + "p99": 0.009153762394562361 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.02981608573929955, + "rms": 0.013225997518156242, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 5.742685293556856e-06, + "rms": 3.9261244221158015e-06, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.04412389833215077, + "rms": 0.02308135101494951, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0074642243022383335, + "rms": 0.003909878845013588, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 80, + "max": 0.005029249936342239, + "mean": 0.0017759860085789115, + "min": 0.0006470419466495514, + "p50": 0.0015880623832345009, + "p95": 0.0034895459422841666, + "p99": 0.004584282496944066 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008326774844434457, + "rms": 0.0003401479099555081, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.03326425696156651, + "rms": 0.022034983995398955, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016646717680845787, + "rms": 0.0011858260546533327, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003453338679850806, + "rms": 0.00024127155621666324, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 40, + "max": 0.014186250045895576, + "mean": 0.005832515482325107, + "min": 0.0024369172751903534, + "p50": 0.005175937432795763, + "p95": 0.011559602105990052, + "p99": 0.013271910920739172 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008302996044630572, + "rms": 0.0003403271747363841, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.06013931453462634, + "rms": 0.04045494765734336, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016591211974573538, + "rms": 0.0011861733552133054, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003442527186140464, + "rms": 0.00024139711439510506, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 20, + "max": 0.012306916061788797, + "mean": 0.005707683321088552, + "min": 0.002658749930560589, + "p50": 0.0055744166020303965, + "p95": 0.009501170669682327, + "p99": 0.0117457669833675 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4000, + "max_abs": 0.0008273125449575409, + "rms": 0.00033949173576754035, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.1282969059974849, + "rms": 0.08070727519558452, + "unit": "rad" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.0016510537681685671, + "rms": 0.0011833645827660944, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0003429001754549268, + "rms": 0.00024080873496737777, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "so101", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 80, + "max": 0.010462332982569933, + "mean": 0.002613240631762892, + "min": 0.0009182081557810307, + "p50": 0.002148041967302561, + "p95": 0.0050189515808597195, + "p99": 0.010418849727138876 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.00924051608824783, + "rms": 0.003941959569853427, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.918050757342074, + "rms": 0.7432963040428657, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.02246886242112752, + "rms": 0.01595557972482612, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010196041453349085, + "rms": 0.007233694287938201, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 2, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 40, + "max": 0.010665165726095438, + "mean": 0.0029893259867094456, + "min": 0.001444416120648384, + "p50": 0.0023972915951162577, + "p95": 0.004724524985067533, + "p99": 0.010085219778120517 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.00924051608824783, + "rms": 0.003941959569853427, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.918050757342074, + "rms": 0.7432963040428657, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.02246886242112752, + "rms": 0.01595557972482612, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010196041453349085, + "rms": 0.007233694287938201, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "hold", + "control_loop_compute_s": { + "count": 20, + "max": 0.03068029135465622, + "mean": 0.00730702702421695, + "min": 0.0036333748139441013, + "p50": 0.006057520629838109, + "p95": 0.011164164682850256, + "p99": 0.026777066020294997 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.00924051608824783, + "rms": 0.003941959569853427, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.918050757342074, + "rms": 0.7432963040428657, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.02246886242112752, + "rms": 0.01595557972482612, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010196041453349085, + "rms": 0.007233694287938201, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 80, + "max": 0.028616415802389383, + "mean": 0.0032046255597379058, + "min": 0.0007987502031028271, + "p50": 0.0017446046695113182, + "p95": 0.009493114170618355, + "p99": 0.016671616816893126 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.049907958494966875, + "rms": 0.02629572986085865, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.91891430947302, + "rms": 0.742849490208355, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.09721126416034358, + "rms": 0.06333744984330507, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.0299272221571862, + "rms": 0.019867215123913897, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 3, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 40, + "max": 0.01701970910653472, + "mean": 0.004860776022542268, + "min": 0.002299458719789982, + "p50": 0.0037744168657809496, + "p95": 0.010586262354627236, + "p99": 0.016492006303742528 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.05093615810435548, + "rms": 0.02692251510465346, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.9189109458097693, + "rms": 0.7428486399409744, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.09963781465616382, + "rms": 0.06484564887474988, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.03065742603190389, + "rms": 0.0203147738467213, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "arm_perturbation", + "control_loop_compute_s": { + "count": 20, + "max": 0.0472996667958796, + "mean": 0.013859724905341864, + "min": 0.0054442500695586205, + "p50": 0.011154124280437827, + "p95": 0.033417355967685594, + "p99": 0.04452320463024078 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.05301414459645004, + "rms": 0.02813457178537373, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 0.9184572014957235, + "rms": 0.742860215181785, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.10446790448844755, + "rms": 0.06777038006399526, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.03209588929058554, + "rms": 0.02118182912139, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 1, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 80, + "max": 0.012316542211920023, + "mean": 0.002022490167291835, + "min": 0.0010337079875171185, + "p50": 0.0017133336514234543, + "p95": 0.0038158030016347756, + "p99": 0.006805337746627585 + }, + "control_period_s": 0.01, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.009242873218211045, + "rms": 0.003942259964223983, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 25.981403055286076, + "rms": 17.411248530599146, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.022475215784776733, + "rms": 0.01595686129225583, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010198672475922427, + "rms": 0.007234223856341337, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 1, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 40, + "max": 0.0027839168906211853, + "mean": 0.0016798335127532482, + "min": 0.0013720830902457237, + "p50": 0.0016017502639442682, + "p95": 0.0022791357943788156, + "p99": 0.002758485428057611 + }, + "control_period_s": 0.02, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.009242955768466565, + "rms": 0.003942226091800963, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 27.278609842746512, + "rms": 18.20819885391918, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.022475424829707862, + "rms": 0.01595671584359386, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010198760817394103, + "rms": 0.007234163990656436, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "ur5e-2f85", + "valid_attempts": 2 + }, + { + "attempts": 2, + "case": "gripper_cycle", + "control_loop_compute_s": { + "count": 20, + "max": 0.0035013328306376934, + "mean": 0.0025757978204637767, + "min": 0.002161750104278326, + "p50": 0.0024899584241211414, + "p95": 0.003410094091668725, + "p99": 0.0034830850828438996 + }, + "control_period_s": 0.04, + "metrics": { + "arm_continuous_reference": { + "count": 4800, + "max_abs": 0.009242921287103467, + "rms": 0.003942153866106038, + "unit": "rad" + }, + "gripper_continuous_reference": { + "count": 800, + "max_abs": 29.99713874092438, + "rms": 19.71692744342938, + "unit": "native_0_255" + }, + "terminal_orientation_continuous_reference": { + "count": 800, + "max_abs": 0.022475292741493097, + "rms": 0.015956405026585237, + "unit": "rad" + }, + "terminal_position_continuous_reference": { + "count": 800, + "max_abs": 0.010198712778583216, + "rms": 0.007234036772854116, + "unit": "m" + } + }, + "offline_control_budget_exceedances": 0, + "profile": "ur5e-2f85", + "valid_attempts": 2 + } + ], + "selection": "All fixed roster attempts retained; no best-profile or period selection, tuning or automatic reruns.", + "valid_attempts": 54 + }, + "observed_step_timing_scope": "native_step_compute_s includes native step, forward update, snapshot/state checks and reference FK; not pure physics time.", + "repeat_scope": "Identical deterministic scenarios; no task generalization or confidence interval.", + "offline_control_budget_exceedances": 21, + "total_control_intervals": 1260 +} diff --git a/docs/arm-interfaces-v01.md b/docs/arm-interfaces-v01.md new file mode 100644 index 0000000..56e6a69 --- /dev/null +++ b/docs/arm-interfaces-v01.md @@ -0,0 +1,173 @@ +# Arm interfaces and skill installation v0.1 + +One local command now describes three native simulation adapters, checks a +learned policy against its exact embodiment, installs its data, and runs an +independently scored attempt. The same package includes a common tracking +benchmark and a small environment-conditioned lift-planning assessment. + +This is a research preview. The three adapters are PiPER, SO-101, and UR5e with +a Robotiq 2F-85. Only PiPER has a learned policy runner in this installer. No +physical arm is connected, calibrated or qualified by these commands. + +## Install locally + +Use Python 3.12 and a fresh virtual environment. This preview is source branch +`feat/arm-interfaces-v01`; the existing stable package and previous preview do +not contain `dvidia-arm`. + +```sh +git clone --branch feat/arm-interfaces-v01 https://github.com/Dvidia-Inference/dvidia-training.git +cd dvidia-training +python3.12 -m venv .venv +source .venv/bin/activate +python -m pip install '.[arm]' +dvidia-arm list +dvidia-arm describe piper +``` + +The CPU simulator needs no CUDA GPU. Setup requires network access; commands +use local, verified assets after provisioning. Hardware minimums and other OS +performance are unqualified. A browser UI can call the same Python functions; +this preview adds Terminal commands and does not change the public website. + +## Three explicit command contracts + +| Adapter | Arm axes | Gripper command | Available here | +| --- | --- | --- | --- | +| `piper` | 6, radians | One coupled finger slide target in metres; not total jaw gap | Native mechanics and existing learned block policy | +| `so101` | 5, radians | Gripper hinge angle in radians | Native mechanics; no learned task runner | +| `ur5e-2f85` | 6, radians | Robotiq native actuator input, 0–255; not metres or newtons | Native mechanics; no learned task runner in this installer | + +Asset identity, ordered actuator names, units, couplings, limits, native version +and compiled model identity travel with the adapter. Unknown commands, wrong +units/order, nonfinite values and limit violations are rejected. Named command +inputs require the complete exact name set. No driver is selected from a model +file and no downloaded code is executed. + +Provision the already pinned models with `dvidia-robot-sim fetch piper` and +`dvidia-robot-sim fetch so101`, each with `--output robot-assets`. The UR5e and +Robotiq reference uses `dvidia-embodiment fetch-assets --output reference-assets`, +documented in [hardware-reference.md](hardware-reference.md). Different roots can be passed +to the shared benchmark using `--reference-assets`. + +## Check, install, then run + +Use the published [PiPER block policy](https://huggingface.co/Dvidia/piper-block-placement-v01). +The exact initial release model is: + +```sh +curl --fail --location 'https://huggingface.co/Dvidia/piper-block-placement-v01/resolve/10edc19c63f04d1428e051787a601e5b69d8a3f5/model/model.json?download=true' --output model.json +printf '%s\n' '2976a605575d9edefb5db366e72c5e006c0ce85a5531ad3d61512436864c8ded model.json' | shasum -a 256 -c - +dvidia-arm check --model model.json --robot piper +dvidia-arm install --model model.json --robot piper --assets robot-assets --output installed-placement +dvidia-arm inspect installed-placement --assets robot-assets +dvidia-arm run installed-placement --assets robot-assets --start-x .309 --start-y -.04 --goal 1 --output placement-attempt +``` + +`install` verifies and copies data without taking physics steps. `run` checks +the installation again, then uses the existing bounded PiPER runner and retains +the trace and independent score. A failed attempt remains a failed attempt. The +output directory must be new. Changing the policy, runtime, assets or contract +requires a new installation. The same policy on `--robot so101` or +`--robot ur5e-2f85` is rejected, even though UR5e also has six arm axes. + +This policy was distilled from synthetic controller demonstrations. The frozen +held-out placement result remains 5/12, versus 12/12 for the authored teacher. +One installation smoke does not update that benchmark. The fresh installed dev3 wheel passed the selected example once, but its full control-step compute p95 was 34.924 ms with 179/1,000 nominal 20 ms budgets exceeded on the shared host; this is not real-time qualification. Its operating envelope +is a 40 g rigid block, initial x=0.284–0.314 m and y=−0.045–−0.015 m, and two +known destinations. The published feed-forward primitive uses initial native +object position, goal and time. It has no learned camera or contact recovery. + +## Measure accuracy and compute separately + +```sh +dvidia-arm benchmark --assets robot-assets --reference-assets reference-assets --output tracking-study +``` + +The shared protocol exercises hold, smooth arm motion and gripper cycling, at +10, 20 and 40 ms control updates with a fixed 1 ms physics step. It preserves +all attempts and compares joint tracking error, gripper tracking in its native +units, terminal-link tracking and loop compute percentiles. Compilation and +loop compute are separate costs. These are unloaded simulated mechanics, not +object-placement success, sensor-to-actuator latency or physical accuracy. + +The [first frozen study](../benchmarks/arm-interfaces-v01/README.md) retained +54/54 numerically valid attempts, 21,654 state rows and 1,260 issued commands. +An independent audit reconstructed 648 RMS/max values without a mismatch. +There were 21 offline compute-budget exceedances. The two repeats share identical +initial conditions; they measure timing variability, not task generalization. +The host was shared, not a dedicated real-time machine. The receipt field +`native_step_compute_s` includes the full observed-step wrapper (forward update, +state snapshot and reference forward kinematics), not isolated physics cost. + +Do not increase a learned policy's speed merely by replaying it faster. Its +training clock, acceleration, contact, braking and recovery requirements need +a new task benchmark. Optimize on development scenes, freeze the choice, then +evaluate untouched confirmation scenes. Report the accuracy/compute tradeoff +and failures rather than collapsing unlike units into a single ranking. + +## A skill needs a suitable scene + +For state `x`, estimated physical properties `theta`, embodiment `r` and task +`g`, applicability is a set `A(g, r, theta)`. The controller should transport +only while its estimated state and uncertainty satisfy the task's conditions. +Probing changes the estimate of `theta`; it is not evidence that the original +estimate was correct. A new object, pose or grasp can invalidate earlier evidence. + +The first planning gate uses a deliberately limited vertical, two-finger +friction model: + +```text +required force = mass upper bound × (gravity + upward acceleration) × safety factor +friction capacity = 2 × friction lower bound × allowable normal force per finger +``` + +It also checks declared reachability, gripper fit, pose uncertainty, freshness, +slip, calibration identity and a pose-specific payload ceiling. Unknown mass or +friction remains unknown. Supplied measurements are caller assertions; the tool +does not authenticate a calibration or measure a robot. + +```sh +dvidia-arm task-template > lift-conditions.json +dvidia-arm assess-task lift-conditions.json +``` + +Results are `blocked`, `probe_required`, `test_lift_required`, or +`ready_for_simulated_transport`. This assessment performs no sensing or motion +and is not wired into the existing learned block controller. It omits moments, +center of mass, deformation, contact damage dynamics and impacts. The input +normal-force budget must already respect the object's allowable load and the +gripper's calibrated capability. A real runtime needs time-aligned evidence, +force limits, slip monitoring and bounded recovery before using this decision. + +The next experiment is to vary object mass, friction, center of mass and sensor +delay independently, then compare an unconditional lift with probe → small lift +→ transport/regrasp/abstain. Measure drops, false secure-grip decisions, overload, +completion, recovery time and observation-to-command latency. Preserve failure +conditions, especially when probing cannot resolve uncertainty. + +## Expansion roadmap + +Add adapters by interface family, then qualify each exact arm/gripper pairing. +The following are integration candidates, not supported DVIDIA hardware: + +| Priority | Candidate family | Upstream interface to build on | +| --- | --- | --- | +| First | SO-100, SO-101; later Koch | [LeRobot robot interface](https://github.com/huggingface/lerobot/blob/main/README.md), with distinct motor maps and per-device calibration | +| First | PiPER and its firmware variants | [AgileX CAN SDK](https://github.com/agilexrobotics/pyAgxArm) and [ROS 2 driver](https://github.com/agilexrobotics/agx_arm_ros) | +| Next | OpenArm, later bimanual configurations | [LeRobot OpenArm guide](https://huggingface.co/docs/lerobot/openarm), CAN and distinct arm/hand calibration | +| Next | UFACTORY xArm variants | [Vendor ROS 2 packages](https://github.com/xArm-Developer/xarm_ros2) | +| Next | Franka research arms | [Vendor ROS 2 integration](https://github.com/frankarobotics/franka_ros2) | +| Next | Interbotix / WidowX family | [Vendor manipulator packages](https://github.com/Interbotix/interbotix_ros_manipulators) | + +[ros2_control](https://control.ros.org/jazzy/doc/ros2_control/hardware_interface/doc/hardware_interface_types_userdoc.html) +provides a common hardware-component vocabulary. It does not erase joint limits, +transmission, firmware, tool, sensor, or task differences. Published software +interfaces also do not establish that every robot's manufacturing design is +open source. Review each dependency's actual license before integration. + +Every new adapter needs a pinned asset/driver inventory, named units and frames, +calibration mapping, stale-state/limit/stop tests, a native mechanics receipt, +and a task-specific policy evaluation. Physical support requires measured +hardware evidence. Cross-arm transfer remains a separate learning and retargeting +problem; it cannot be solved by renaming joint fields. diff --git a/publishing/huggingface/publish_arm_interfaces_v01.py b/publishing/huggingface/publish_arm_interfaces_v01.py new file mode 100644 index 0000000..f9d2746 --- /dev/null +++ b/publishing/huggingface/publish_arm_interfaces_v01.py @@ -0,0 +1,117 @@ +"""Add one bounded public benchmark dataset; never modify shared Spaces. + +Uses the existing named Keychain client only during publish. Credentials remain +in process memory; remote failures produce fixed diagnostics, not raw responses. +""" +from __future__ import annotations + +import argparse +from hashlib import sha256 +import json +from pathlib import Path +import sys + +from publish import PublicationError, client +from publish_task_environments_v02 import digest, encoded, files, plain, read, public_client, download + +REPO = 'Dvidia/arm-interfaces-v01' +SCHEMA = 'dvidia.arm-interfaces-publication/v1' +NAMES = {'README.md', 'LICENSE', 'arm-interfaces-v01-evidence.zip', + 'summary.json', 'independent-audit.json', 'installation-smoke.json'} + + +def stage(root, plan): + root, plan = plain(root), plain(plan) + inventory = files(root) + if {r['path'] for r in inventory} != NAMES: + raise PublicationError('Dataset must contain exactly the reviewed six files.') + value = {'schema': SCHEMA, 'repo_id': REPO, 'repo_type': 'dataset', + 'private': False, 'root': str(root), 'files': inventory} + with plan.open('xb') as stream: + stream.write(encoded(value)) + return {'plan_sha256': sha256(read(plan)).hexdigest(), 'files': len(inventory), + 'bytes': sum(r['bytes'] for r in inventory)} + + +def checked(plan, expected_digest): + raw = read(plan) + if sha256(raw).hexdigest() != expected_digest: + raise PublicationError('Reviewed plan digest changed.') + value = json.loads(raw) + if (set(value) != {'schema', 'repo_id', 'repo_type', 'private', 'root', 'files'} + or value['schema'] != SCHEMA or value['repo_id'] != REPO + or value['repo_type'] != 'dataset' or value['private'] is not False + or {r['path'] for r in value['files']} != NAMES + or files(value['root']) != value['files']): + raise PublicationError('Reviewed dataset identity or bytes changed.') + return value + + +def publish(plan, expected_digest, cache): + value = checked(plan, expected_digest) + payloads = [] + for row in value['files']: + raw = read(Path(value['root'])/row['path']) + if digest(raw) != {key: row[key] for key in ('bytes', 'sha256')}: + raise PublicationError('Payload changed while freezing reviewed publication bytes.') + payloads.append((row['path'], raw)) + api, token = client(cache) + from huggingface_hub import CommitOperationAdd + from huggingface_hub.errors import RepositoryNotFoundError + try: + info = api.repo_info(REPO, repo_type='dataset', files_metadata=True) + except RepositoryNotFoundError: + api.create_repo(REPO, repo_type='dataset', private=False, exist_ok=False) + info = api.repo_info(REPO, repo_type='dataset', files_metadata=True) + if info.private is not False or not info.sha or not info.siblings: + raise PublicationError('Expected an initialized public dataset.') + existing = {item.rfilename for item in info.siblings} + if existing != {'.gitattributes'}: + raise PublicationError('Dataset is not empty. Verify the existing publication; do not overwrite it.') + # Use the exact immutable bytes verified before credential/remote access. + operations = [CommitOperationAdd(path_in_repo=name, path_or_fileobj=raw) + for name, raw in payloads] + commit = api.create_commit(REPO, repo_type='dataset', operations=operations, + parent_commit=info.sha, + commit_message='Publish three-arm native interface benchmark and installation evidence') + del token, api + return {'repo_id': REPO, 'commit': commit.oid, 'files_added': len(operations)} + + +def verify(plan, expected_digest, cache): + value = checked(plan, expected_digest) + api = public_client() + info = api.repo_info(REPO, repo_type='dataset', files_metadata=True, token=False) + if info.private is not False or {r.rfilename for r in info.siblings} != NAMES | {'.gitattributes'}: + raise PublicationError('Public dataset visibility or roster differs.') + for row in value['files']: + raw = download(REPO, row['path'], 'dataset', info.sha, cache) + if digest(raw) != {key: row[key] for key in ('bytes', 'sha256')}: + raise PublicationError('Anonymous public bytes differ from the reviewed artifacts.') + return {'repo_id': REPO, 'commit': info.sha, 'anonymous_files_verified': len(value['files'])} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest='command', required=True) + s = commands.add_parser('stage'); s.add_argument('root'); s.add_argument('--plan', required=True) + for name in ('publish', 'verify'): + command = commands.add_parser(name) + command.add_argument('--plan', required=True) + command.add_argument('--sha256', required=True) + command.add_argument('--cache', required=True) + args = parser.parse_args() + try: + result = (stage(args.root, args.plan) if args.command == 'stage' else + globals()[args.command](args.plan, args.sha256, Path(args.cache))) + print(json.dumps(result, indent=2)) + return 0 + except PublicationError as exc: + print(str(exc), file=sys.stderr) + except Exception: + print('Publication operation failed; no credential or remote response details displayed.', file=sys.stderr) + return 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/publishing/huggingface/test_publish_arm_interfaces_v01.py b/publishing/huggingface/test_publish_arm_interfaces_v01.py new file mode 100644 index 0000000..9f9049c --- /dev/null +++ b/publishing/huggingface/test_publish_arm_interfaces_v01.py @@ -0,0 +1,71 @@ +from copy import deepcopy +from hashlib import sha256 +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +import publish_arm_interfaces_v01 as publisher + + +class PublicationTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name).resolve() + self.dataset = self.root/'dataset'; self.dataset.mkdir() + for name in publisher.NAMES: + (self.dataset/name).write_bytes(b'synthetic publication fixture') + self.plan = self.root/'plan.json' + self.receipt = publisher.stage(self.dataset, self.plan) + + def test_staging_and_checking_are_credential_free(self): + with patch.object(publisher, 'client', side_effect=AssertionError('Credential accessed')): + result = publisher.checked(self.plan, self.receipt['plan_sha256']) + self.assertEqual(result['repo_id'], 'Dvidia/arm-interfaces-v01') + self.assertEqual(len(result['files']), 6) + + def test_mutated_payload_and_unreviewed_extra_files_fail(self): + (self.dataset/'README.md').write_bytes(b'changed') + with self.assertRaises(publisher.PublicationError): + publisher.checked(self.plan, self.receipt['plan_sha256']) + (self.dataset/'README.md').write_bytes(b'synthetic publication fixture') + (self.dataset/'unreviewed.json').write_text('{}') + with self.assertRaises(publisher.PublicationError): + publisher.checked(self.plan, self.receipt['plan_sha256']) + + def test_resealed_changed_destination_is_rejected(self): + value = json.loads(self.plan.read_bytes()) + for field, replacement in [('repo_id', 'Dvidia/robot-simulation-lab'), + ('repo_type', 'space'), ('private', True)]: + changed = deepcopy(value); changed[field] = replacement + raw = publisher.encoded(changed); self.plan.write_bytes(raw) + with self.assertRaises(publisher.PublicationError): + publisher.checked(self.plan, sha256(raw).hexdigest()) + + def test_existing_dataset_cannot_be_overwritten(self): + class Info: + private = False + sha = 'a'*40 + siblings = [type('File', (), {'rfilename': 'README.md'})()] + with patch.object(publisher, 'client') as get_client: + api = unittest.mock.Mock() + api.repo_info.return_value = Info() + get_client.return_value = api, 'synthetic-fixture' + with self.assertRaisesRegex(publisher.PublicationError, 'not empty'): + publisher.publish(self.plan, self.receipt['plan_sha256'], self.root/'cache') + api.create_commit.assert_not_called() + + def test_mutation_at_payload_freeze_is_rejected_before_credentials(self): + original = publisher.read + def changed(path, *args, **kwargs): + return b'changed after review' if Path(path).name == 'README.md' else original(path, *args, **kwargs) + with patch.object(publisher, 'read', side_effect=changed), patch.object(publisher, 'client') as client: + with self.assertRaisesRegex(publisher.PublicationError, 'freezing'): + publisher.publish(self.plan, self.receipt['plan_sha256'], self.root/'cache') + client.assert_not_called() + + +if __name__ == '__main__': + unittest.main() diff --git a/pyproject.toml b/pyproject.toml index 4d65d5d..487c341 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dvidia-training" -version = "0.1.3.dev2" +version = "0.1.3.dev3" description = "Local CPU training from Skillspace footage, with optional simulation movement candidates" readme = "README.md" license = "MIT AND Apache-2.0" @@ -28,6 +28,7 @@ Repository = "https://github.com/Dvidia-Inference/dvidia-training" Issues = "https://github.com/Dvidia-Inference/dvidia-training/issues" [project.scripts] +dvidia-arm = "dvidia_training.arm_interface_cli:main" dvidia-train = "dvidia_training.footage_pipeline:main" dvidia-training-studio = "dvidia_training.training_studio:main" dvidia-training-doctor = "dvidia_training.doctor:main" diff --git a/src/dvidia_training/__init__.py b/src/dvidia_training/__init__.py index 860747a..d0ddab4 100644 --- a/src/dvidia_training/__init__.py +++ b/src/dvidia_training/__init__.py @@ -1,2 +1,2 @@ """CPU Skillspace footage learning with optional simulation movement tools.""" -__version__ = "0.1.3.dev2" +__version__ = "0.1.3.dev3" diff --git a/src/dvidia_training/arm_interface_benchmark.py b/src/dvidia_training/arm_interface_benchmark.py new file mode 100644 index 0000000..fa9f588 --- /dev/null +++ b/src/dvidia_training/arm_interface_benchmark.py @@ -0,0 +1,383 @@ +"""Bounded, unloaded native CPU tracking checks for explicit arm interfaces. + +This is an unpaced simulator microbenchmark. It records privileged state and +authored setpoints; it does not measure a sensor, bus, physical actuator or task. +""" +from __future__ import annotations + +from collections.abc import Mapping +from hashlib import sha256 +import json +import math +from pathlib import Path +import platform +import time + +SCHEMA = "dvidia.arm-interface-benchmark/v1" +PROFILES = ("piper", "so101", "ur5e-2f85") +CASES = ("hold", "arm_perturbation", "gripper_cycle") +PERIODS = (.01, .02, .04) +TIMESTEP = .001 +SCOPE = ("Unloaded native CPU tracking of authored setpoints using pinned upstream mechanics. " + "Privileged simulator state; unpaced offline computation. No object task, learned policy, " + "hardware latency, calibrated jaw gap/force or physical robot qualification.") + + +def _interfaces(): + from . import arm_interfaces + return arm_interfaces + + +def _settings(profiles, duration, repeats): + if not isinstance(profiles, (list, tuple)) or not 1 <= len(profiles) <= 3: + raise ValueError("profiles must contain one to three known unique interfaces") + if any(not isinstance(p, str) or p not in PROFILES for p in profiles) or len(set(profiles)) != len(profiles): + raise ValueError("profiles must contain one to three known unique interfaces") + if isinstance(duration, bool) or not isinstance(duration, (int, float)) or not math.isfinite(duration) or not .4 <= duration <= 2: + raise ValueError("duration must be finite and within [0.4,2] seconds") + if not math.isclose(duration/.04, round(duration/.04), rel_tol=0, abs_tol=1e-9): + raise ValueError("duration must contain complete 40 ms periods") + if type(repeats) is not int or not 1 <= repeats <= 3: + raise ValueError("repeats must be an integer within [1,3]") + return {"profiles": list(profiles), "duration_s": float(duration), "repeats": repeats, + "cases": list(CASES), "control_periods_s": list(PERIODS), "physics_timestep_s": TIMESTEP, + "arm_amplitude_limit_rad": .04, "gripper_cycle_fraction_of_range": .2, + "waveforms": {"arm": "home + signed amplitude*sin(2*pi*t/duration)", + "gripper": "home + amplitude*sin(pi*t/duration)^2"}, + "command_sampling": "At interval start; held until the next control boundary. No terminal command after the horizon.", + "accuracy": "Every post-step native millisecond, against both the applied held target and continuous reference at observation time.", + "pacing": "Unpaced offline; budget exceedances compare computation with nominal control periods only."} + + +def _json(value, *, compact=False): + return (json.dumps(value, allow_nan=False, sort_keys=True, + indent=None if compact else 2, + separators=(",", ":") if compact else None)+"\n").encode() + + +def _write(path, value, *, compact=False): + content = _json(value, compact=compact) + with Path(path).open("xb") as stream: + stream.write(content) + return {"bytes": len(content), "sha256": sha256(content).hexdigest()} + + +def _source_snapshot(output): + root = Path(__file__).parent + names = ("arm_interface_benchmark.py", "arm_interfaces.py", "asset_library.py", "asset_catalog.json", + "reference_sim.py", "reference_assets.lock.json", "robot_profiles.py", "robot_profiles.lock.json", "env.py") + source = output/"source" + source.mkdir() + inventory = [] + for name in names: + raw = (root/name).read_bytes() + with (source/name).open("xb") as stream: + stream.write(raw) + inventory.append({"name": name, "bytes": len(raw), "sha256": sha256(raw).hexdigest()}) + return inventory + + +def _check_sources(inventory): + root = Path(__file__).parent + if any(sha256((root/row["name"]).read_bytes()).hexdigest() != row["sha256"] for row in inventory): + raise RuntimeError("Implementation changed after the benchmark protocol was frozen") + + +def _percentiles(values): + ordered = sorted(values) + if not ordered: + return {"count": 0, "min": None, "p50": None, "p95": None, "p99": None, "max": None, "mean": None} + def quantile(p): + rank = (len(ordered)-1)*p + low, high = math.floor(rank), math.ceil(rank) + return ordered[low]+(ordered[high]-ordered[low])*(rank-low) + return {"count": len(ordered), "min": ordered[0], "p50": quantile(.5), + "p95": quantile(.95), "p99": quantile(.99), "max": ordered[-1], "mean": sum(ordered)/len(ordered)} + + +def _amplitudes(home, limits, arm_indices, gripper_indices): + amplitudes = [0.]*len(home) + for index in arm_indices: + low, high = limits[index] + room = min(home[index]-low, high-home[index]) + if room < 0: + raise ValueError("Home command exceeds interface limits") + amplitudes[index] = min(.04, .8*room)*(-1 if index % 2 else 1) + for index in gripper_indices: + low, high = limits[index] + if not low <= home[index] <= high: + raise ValueError("Home gripper command exceeds interface limits") + amplitudes[index] = min(.2*(high-low), .8*(high-home[index])) + return amplitudes + + +def _command(case, elapsed, duration, home, amplitudes, arm_indices, gripper_indices): + if case not in CASES: + raise ValueError("Unknown tracking case") + result = list(home) + indices = arm_indices if case == "arm_perturbation" else gripper_indices if case == "gripper_cycle" else () + phase = math.sin(2*math.pi*elapsed/duration) if case == "arm_perturbation" else math.sin(math.pi*elapsed/duration)**2 + for index in indices: + result[index] += amplitudes[index]*phase + return result + + +def _servo_equivalent(length, gain, bias0, bias1): + """Static position-servo coordinate in the native command's own units. + + Affine actuator force = gain*ctrl + bias0 + bias1*length + damping*velocity. + This is a compiled-model coordinate transformation, not a jaw-gap sensor. + """ + if not all(math.isfinite(x) for x in (length, gain, bias0, bias1)) or gain <= 0 or bias1 >= 0: + raise ValueError("Unsupported or nonfinite position-servo mapping") + return -(bias0+bias1*length)/gain + + +def _error_summary(values, unit): + if not values: + return {"unit": unit, "count": 0, "rms": None, "max_abs": None} + if not all(math.isfinite(x) for x in values): + raise ValueError("Cannot summarize nonfinite errors") + return {"unit": unit, "count": len(values), "rms": math.sqrt(sum(x*x for x in values)/len(values)), + "max_abs": max(abs(x) for x in values)} + + +def _metrics(rows, gripper_unit): + errors = [row["tracking_error"] for row in rows] + return {"state_rows": len(rows), + "arm_held_target": _error_summary([v for e in errors for v in e["arm_held_target_rad"]], "rad"), + "arm_continuous_reference": _error_summary([v for e in errors for v in e["arm_continuous_reference_rad"]], "rad"), + "gripper_held_target": _error_summary([e["gripper_held_target_native"] for e in errors], gripper_unit), + "gripper_continuous_reference": _error_summary([e["gripper_continuous_reference_native"] for e in errors], gripper_unit), + "terminal_position_continuous_reference": _error_summary([e["terminal_position_reference_m"] for e in errors], "m"), + "terminal_orientation_continuous_reference": _error_summary([e["terminal_orientation_reference_rad"] for e in errors], "rad")} + + +def _snapshot(mj, np, model, data, reference_data, contract, arm_qpos, held, reference, elapsed, gripper): + arrays = (data.qpos, data.qvel, data.qacc, data.xpos, data.xquat, data.actuator_length, data.actuator_force) + if not all(np.isfinite(value).all() for value in arrays): + raise RuntimeError("Native state became nonfinite") + arm_indices = contract["arm_action_indices"] + body = mj.mj_name2id(model, mj.mjtObj.mjOBJ_BODY, contract["tool_frame_body"]) + if body <= 0: + raise ValueError("Interface terminal link is missing") + reference_data.qpos[:] = data.qpos + for action, qpos in zip(arm_indices, arm_qpos): + reference_data.qpos[qpos] = reference[action] + mj.mj_kinematics(model, reference_data) + position_error = float(np.linalg.norm(data.xpos[body]-reference_data.xpos[body])) + dot = abs(float(np.dot(data.xquat[body], reference_data.xquat[body]))) + orientation_error = 2*math.acos(min(1., max(0., dot))) + jaw = gripper["action_index"] + jaw_equivalent = _servo_equivalent(float(data.actuator_length[jaw]), gripper["gain"], gripper["bias0"], gripper["bias1"]) + arm = [float(data.qpos[q]) for q in arm_qpos] + return {"elapsed_s": elapsed, "native_time_s": float(data.time), + "observation_origin": "privileged_native_simulator_state", "applied_command_native": list(held), + "continuous_reference_command_native": reference, "qpos_native": data.qpos.tolist(), + "qvel_native_per_s": data.qvel.tolist(), "arm_position_rad": arm, + "gripper_actuator_length_native": float(data.actuator_length[jaw]), + "gripper_servo_equivalent_position_native": jaw_equivalent, + "actuator_force_native": data.actuator_force.tolist(), "contact_count": int(data.ncon), + "terminal_link_position_m": data.xpos[body].tolist(), + "terminal_link_quaternion_wxyz": data.xquat[body].tolist(), + "reference_terminal_link_position_m": reference_data.xpos[body].tolist(), + "reference_terminal_link_quaternion_wxyz": reference_data.xquat[body].tolist(), + "tracking_error": { + "arm_held_target_rad": [value-held[action] for value, action in zip(arm, arm_indices)], + "arm_continuous_reference_rad": [value-reference[action] for value, action in zip(arm, arm_indices)], + "gripper_held_target_native": jaw_equivalent-held[jaw], + "gripper_continuous_reference_native": jaw_equivalent-reference[jaw], + "terminal_position_reference_m": position_error, "terminal_orientation_reference_rad": orientation_error}} + + +def _safe_state(data): + if data is None: + return None + def numbers(values): + return [float(v) if math.isfinite(float(v)) else repr(float(v)) for v in values] + return {"native_time_s": numbers([data.time])[0], "qpos_native": numbers(data.qpos), + "qvel_native_per_s": numbers(data.qvel), "last_command_native": numbers(data.ctrl)} + + +def _attempt(item, assets, folder, settings, inventory): + from . import reference_sim as sim + begun = time.perf_counter() + result = {**item, "valid": False, "error": None, "warnings": [], "warning_counters": [], + "metrics": {}, "timing": {}, "simulated_seconds": 0., "failure_state": None} + _write(folder/"started.json", {"schema": SCHEMA, "attempt": item, "state": "started", "unix_ns": time.time_ns()}) + data, model, loop_started = None, None, None + rows, commands, loop_times, step_times = [], [], [], [] + build_seconds, loop_seconds, stage = None, None, "source_check" + try: + _check_sources(inventory) + stage = "build" + with sim.native_warnings(result["warnings"]): + build_started = time.perf_counter() + model, data, contract = _interfaces().build(item["profile"], assets, timestep=TIMESTEP) + build_seconds = time.perf_counter()-build_started + mj, np = sim._native_imports() + result["contract"] = contract + arm_indices, jaw_indices = contract["arm_action_indices"], contract["gripper_action_indices"] + if len(jaw_indices) != 1: + raise ValueError("Benchmark requires one explicit coupled gripper action") + if any(contract["action_units"][i] != "rad" for i in arm_indices): + raise ValueError("Benchmark arm tracking requires angular commands") + arm_qpos = [] + for action in arm_indices: + if int(model.actuator_trntype[action]) != int(mj.mjtTrn.mjTRN_JOINT): + raise ValueError("Arm actuator must map to a direct scalar joint") + joint = int(model.actuator_trnid[action, 0]) + if int(model.jnt_type[joint]) != int(mj.mjtJoint.mjJNT_HINGE) or not math.isclose(float(model.actuator_gear[action, 0]), 1., abs_tol=1e-12): + raise ValueError("Unsupported arm command coordinate") + arm_qpos.append(int(model.jnt_qposadr[joint])) + jaw = jaw_indices[0] + if int(model.actuator_gaintype[jaw]) != int(mj.mjtGain.mjGAIN_FIXED) or int(model.actuator_biastype[jaw]) != int(mj.mjtBias.mjBIAS_AFFINE) or int(model.actuator_dyntype[jaw]) != int(mj.mjtDyn.mjDYN_NONE): + raise ValueError("Unsupported gripper servo dynamics") + gripper = {"action_index": jaw, "unit": contract["action_units"][jaw], + "gain": float(model.actuator_gainprm[jaw, 0]), "bias0": float(model.actuator_biasprm[jaw, 0]), + "bias1": float(model.actuator_biasprm[jaw, 1]), "damping_bias": float(model.actuator_biasprm[jaw, 2]), + "coordinate": "-(bias0+bias1*actuator_length)/gain; static servo-equivalent command coordinate, not jaw gap or calibrated force"} + _servo_equivalent(0., gripper["gain"], gripper["bias0"], gripper["bias1"]) + home = data.ctrl.tolist() + amplitudes = _amplitudes(home, model.actuator_ctrlrange.tolist(), arm_indices, jaw_indices) + result.update(home_command_native=home, amplitude_native=amplitudes, gripper_coordinate=gripper, + native_model_sha256=sim.model_digest(model), + runtime={"mujoco": mj.__version__, "numpy": np.__version__}, + model_settings={"gravity_m_s2": model.opt.gravity.tolist(), + "gravity_compensation_by_body": model.body_gravcomp.tolist(), + "integrator": int(model.opt.integrator), "timestep_s": float(model.opt.timestep)}) + reference_data = mj.MjData(model) + held = list(home) + rows.append(_snapshot(mj, np, model, data, reference_data, contract, arm_qpos, held, home, 0., gripper)) + if result["warnings"] or any(w.number for w in data.warning): + raise RuntimeError("Native initialization emitted a warning") + intervals, substeps = round(settings["duration_s"]/item["control_period_s"]), round(item["control_period_s"]/TIMESTEP) + stage, loop_started = "control_loop", time.perf_counter() + for interval in range(intervals): + tick = time.perf_counter() + elapsed = interval*item["control_period_s"] + held = _interfaces().validate_command(contract, model, _command(item["case"], elapsed, settings["duration_s"], home, amplitudes, arm_indices, jaw_indices)) + commands.append({"elapsed_s": elapsed, "native_time_s": float(data.time), "values_native": held}) + data.ctrl[:] = held + for offset in range(substeps): + step_started = time.perf_counter() + mj.mj_step(model, data) + mj.mj_forward(model, data) + actual_elapsed = (interval*substeps+offset+1)*TIMESTEP + reference = _command(item["case"], actual_elapsed, settings["duration_s"], home, amplitudes, arm_indices, jaw_indices) + rows.append(_snapshot(mj, np, model, data, reference_data, contract, arm_qpos, held, reference, actual_elapsed, gripper)) + result["simulated_seconds"] = actual_elapsed + if result["warnings"] or any(w.number for w in data.warning): + raise RuntimeError("Native physics emitted a warning") + step_times.append(time.perf_counter()-step_started) + loop_times.append(time.perf_counter()-tick) + loop_seconds = time.perf_counter()-loop_started + result["metrics"] = _metrics(rows[1:], gripper["unit"]) + result["valid"] = True + except Exception as exc: + if loop_started is not None and loop_seconds is None: + loop_seconds = time.perf_counter()-loop_started + result["error"] = {"stage": stage, "type": type(exc).__name__, "message": str(exc)[:1000]} + result["failure_state"] = _safe_state(data) + if data is not None: + result["warning_counters"] = [{"index": i, "number": int(w.number), "lastinfo": int(w.lastinfo)} for i, w in enumerate(data.warning) if w.number] + trace = {"schema": "dvidia.arm-interface-trace/v1", "attempt": item, "rows": rows, "commands": commands, + "scope": SCOPE, "initial_row_in_accuracy": False, "terminal_link_scope": "Native link origin; no calibrated tool centre point"} + trace_identity = _write(folder/"trace.json", trace, compact=True) + result["trace"] = {"path": "trace.json", **trace_identity, "state_rows": len(rows), "commands": len(commands)} + result["timing"] = {"compile_and_initialize_wall_s": build_seconds, + "compile_timing_scope": "arm_interfaces.build, including pinned asset verification, compilation and initialization", + "loop_wall_s": loop_seconds, "control_loop_compute_s": _percentiles(loop_times), + "native_step_compute_s": _percentiles(step_times), "control_intervals_s": loop_times, + "native_steps_s": step_times, "offline_control_budget_exceedances": sum(t > item["control_period_s"] for t in loop_times), + "simulated_seconds_per_loop_wall_second": result["simulated_seconds"]/loop_seconds if loop_seconds else None, + "total_before_result_write_wall_s": time.perf_counter()-begun, + "scope": "Unpaced wall computation; loop includes native stepping, commands, snapshots and reference FK; excludes trace serialization. No hardware or sensor latency."} + _write(folder/"result.json", result) + return result + + +def _summary(attempts): + groups = [] + for profile in dict.fromkeys(row["profile"] for row in attempts): + for case in CASES: + for period in PERIODS: + selected = [a for a in attempts if (a["profile"], a["case"], a["control_period_s"]) == (profile, case, period)] + valid = [a for a in selected if a["valid"]] + metrics = {} + for key in ("arm_continuous_reference", "gripper_continuous_reference", "terminal_position_continuous_reference", "terminal_orientation_continuous_reference"): + rows = [a["metrics"][key] for a in valid] + count = sum(r["count"] for r in rows) + metrics[key] = {"unit": rows[0]["unit"] if rows else None, "count": count, + "rms": math.sqrt(sum(r["rms"]**2*r["count"] for r in rows)/count) if count else None, + "max_abs": max((r["max_abs"] for r in rows), default=None)} + groups.append({"profile": profile, "case": case, "control_period_s": period, + "valid_attempts": len(valid), "attempts": len(selected), "metrics": metrics, + "control_loop_compute_s": _percentiles([t for a in valid for t in a["timing"]["control_intervals_s"]]), + "offline_control_budget_exceedances": sum(a["timing"]["offline_control_budget_exceedances"] for a in valid)}) + return {"attempts": len(attempts), "valid_attempts": sum(a["valid"] for a in attempts), + "failed_attempts": sum(not a["valid"] for a in attempts), "groups": groups, + "selection": "All fixed roster attempts retained; no best-profile or period selection, tuning or automatic reruns."} + + +def _markdown(report): + lines = ["# Arm interface CPU tracking v0.1", "", SCOPE, "", + f"{report['summary']['valid_attempts']}/{report['summary']['attempts']} numerically valid attempts. All attempts, warnings, commands and millisecond state traces are retained.", "", + "| Profile | Case | Update ms | Valid | Arm reference RMS rad | Gripper reference RMS (native unit) | Control compute p95 ms | Offline budget exceeds |", + "| --- | --- | ---: | ---: | ---: | --- | ---: | ---: |"] + def fmt(value): + return "—" if value is None else f"{value:.6g}" + for row in report["summary"]["groups"]: + arm, jaw = row["metrics"]["arm_continuous_reference"], row["metrics"]["gripper_continuous_reference"] + p95 = row["control_loop_compute_s"]["p95"] + lines.append(f"| {row['profile']} | {row['case']} | {row['control_period_s']*1000:g} | {row['valid_attempts']}/{row['attempts']} | {fmt(arm['rms'])} | {fmt(jaw['rms'])} {jaw['unit'] or ''} | {fmt(p95*1000 if p95 is not None else None)} | {row['offline_control_budget_exceedances']} |") + lines += ["", "Arm radians and each gripper's native coordinate are reported separately. The Robotiq 0–255 feedback is derived from its pinned affine servo model, not a measured aperture or force.", "", + "Terminal-link reference errors use forward kinematics at the continuous desired arm configuration. They are native link-origin diagnostics, not calibrated end-effector accuracy.", "", + "Compile timing includes asset verification and initialization. Loop timing includes commands, native stepping, millisecond snapshots and reference FK; trace serialization is outside the loop. Total timing stops before writing each result receipt.", "", + "These unloaded authored trajectories do not demonstrate task success, learned skill transfer, physical accuracy or hard real-time control.", ""] + return "\n".join(lines) + + +def benchmark(assets, output, *, profiles=PROFILES, duration=.4, repeats=2): + """Run a fresh fixed roster; assets may be one root or a profile-to-root map.""" + settings = _settings(profiles, duration, repeats) + if isinstance(assets, Mapping): + if any(p not in assets for p in profiles) or any(p not in PROFILES for p in assets): + raise ValueError("assets map must provide every selected known interface") + roots = {profile: assets[profile] for profile in profiles} + else: + roots = dict.fromkeys(profiles, assets) + destination = Path(output) + if destination.exists() or destination.is_symlink(): + raise ValueError("Benchmark output must be a fresh directory") + destination.mkdir(parents=True, exist_ok=False) + started = time.perf_counter() + inventory = _source_snapshot(destination) + schedule = [{"id": f"attempt-{index:03d}", "profile": profile, "case": case, + "control_period_s": period, "repeat": repeat} + for index, (profile, case, period, repeat) in enumerate( + (p, c, dt, r) for p in profiles for c in CASES for dt in PERIODS for r in range(repeats))] + protocol = {"schema": SCHEMA, "scope": SCOPE, "settings": settings, "source_inventory": inventory, + "schedule": schedule, "platform": {"system": platform.system(), "machine": platform.machine(), + "python": platform.python_version()}, + "task_qualified": False, "physical_qualified": False, + "failure_policy": "Each scheduled attempt gets a started receipt before construction. Errors/warnings are retained; no retries or hidden settling."} + protocol_identity = _write(destination/"protocol.json", protocol) + _write(destination/"execution-intent.json", {"schema": SCHEMA, "protocol": protocol_identity, + "scheduled_attempts": len(schedule), "started_unix_ns": time.time_ns()}) + attempts = [] + for item in schedule: + folder = destination/item["id"] + folder.mkdir() + attempts.append(_attempt(item, roots[item["profile"]], folder, settings, inventory)) + report = {"schema": SCHEMA, "scope": SCOPE, "protocol": protocol_identity, "settings": settings, + "source_inventory": inventory, "platform": protocol["platform"], "attempts": attempts, + "summary": _summary(attempts), "task_qualified": False, "physical_qualified": False, + "total_to_report_write_wall_s": time.perf_counter()-started} + report_identity = _write(destination/"report.json", report) + with (destination/"README.md").open("x", encoding="utf-8") as stream: + stream.write(_markdown(report)) + _write(destination/"completion.json", {"schema": SCHEMA, "report": report_identity, + "scheduled_attempts": len(schedule), "recorded_attempts": len(attempts), + "valid_attempts": report["summary"]["valid_attempts"], "state": "complete"}) + return report diff --git a/src/dvidia_training/arm_interface_cli.py b/src/dvidia_training/arm_interface_cli.py new file mode 100644 index 0000000..f8e3931 --- /dev/null +++ b/src/dvidia_training/arm_interface_cli.py @@ -0,0 +1,89 @@ +"""One local interface for arm discovery, policy installation and diagnostics.""" +from __future__ import annotations + +import argparse +import json +from pathlib import Path +import sys + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + commands = parser.add_subparsers(dest='command', required=True) + commands.add_parser('list', help='List implemented native simulation adapters; no hardware connection.') + describe = commands.add_parser('describe') + describe.add_argument('robot') + for name in ('check', 'install'): + command = commands.add_parser(name) + command.add_argument('--model', type=Path, required=True) + command.add_argument('--robot', required=True) + command.add_argument('--interface', default='mujoco-position-v1') + if name == 'install': + command.add_argument('--assets', type=Path, required=True) + command.add_argument('--output', type=Path, required=True) + inspect = commands.add_parser('inspect') + inspect.add_argument('installation', type=Path) + inspect.add_argument('--assets', type=Path, required=True) + run = commands.add_parser('run') + run.add_argument('installation', type=Path) + run.add_argument('--assets', type=Path, required=True) + run.add_argument('--output', type=Path, required=True) + run.add_argument('--start-x', type=float, default=.30) + run.add_argument('--start-y', type=float, default=-.03) + run.add_argument('--goal', type=int, choices=(0, 1), default=0) + bench = commands.add_parser('benchmark') + bench.add_argument('--assets', type=Path, required=True) + bench.add_argument('--reference-assets', type=Path) + bench.add_argument('--robots', nargs='+', default=['piper', 'so101', 'ur5e-2f85']) + bench.add_argument('--duration', type=float, default=.4) + bench.add_argument('--repeats', type=int, default=2) + bench.add_argument('--output', type=Path, required=True) + task = commands.add_parser('assess-task', help='Assess supplied lift conditions; no robot commands.') + task.add_argument('document', type=Path) + commands.add_parser('task-template', help='Print an explicitly synthetic lift-planning example.') + args = parser.parse_args(argv) + try: + if args.command in ('list', 'describe'): + from .arm_interfaces import catalog, contract + result = catalog() if args.command == 'list' else contract(args.robot) + elif args.command == 'check': + from .arm_skill_install import check_model + result = check_model(args.model, args.robot, interface=args.interface) + elif args.command == 'install': + from .arm_skill_install import install_model + result = install_model(args.model, args.robot, args.assets, args.output, interface=args.interface) + elif args.command == 'inspect': + from .arm_skill_install import inspect_installation + result = inspect_installation(args.installation, args.assets) + elif args.command == 'run': + from .arm_skill_install import run_installation + result = run_installation(args.installation, args.assets, args.output, + start_x=args.start_x, start_y=args.start_y, goal=args.goal) + print(json.dumps(result, indent=2, allow_nan=False)) + return 0 if result['success'] else 1 + elif args.command == 'benchmark': + from .arm_interface_benchmark import benchmark + assets = {name: args.reference_assets if name == 'ur5e-2f85' and args.reference_assets else args.assets + for name in args.robots} + result = benchmark(assets, args.output, profiles=args.robots, + duration=args.duration, repeats=args.repeats) + summary = result['summary'] + print(json.dumps({'report': str(args.output/'report.json'), + 'attempts': summary['attempts'], + 'valid_attempts': summary['valid_attempts'], + 'failed_attempts': summary['failed_attempts'], + 'physical_robot_ready': False}, indent=2)) + return 0 if summary['failed_attempts'] == 0 else 1 + else: + from .task_feasibility import assess, template + from .reference_replay import _read, _decode + result = template() if args.command == 'task-template' else assess(_decode(_read(args.document, 64*1024))) + print(json.dumps(result, indent=2, allow_nan=False)) + return 0 + except (ValueError, OSError, TypeError, KeyError, ImportError) as exc: + print(f'Arm request rejected: {exc}', file=sys.stderr) + return 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/src/dvidia_training/arm_interfaces.py b/src/dvidia_training/arm_interfaces.py new file mode 100644 index 0000000..b8cff4c --- /dev/null +++ b/src/dvidia_training/arm_interfaces.py @@ -0,0 +1,334 @@ +"""Exact native MuJoCo command contracts for three pinned robot models. + +Listing and describing an interface require only the standard library. Building +uses the existing byte-verified asset loaders and returns a runtime-bound +contract. Commands are native position-servo inputs, not Cartesian targets, +measured jaw gaps, calibrated hardware commands, or portable learned skills. + +There is deliberately no hardware transport or command application here. The +caller owns stepping and may assign a validated vector to ``data.ctrl``. Models +returned by build must remain owned by that caller and must not be modified. +Validation checks command topology and servo settings on every call; it does +not rehash the complete mesh-bearing native model on every control boundary. +""" +from __future__ import annotations + +from copy import deepcopy +from functools import lru_cache +from hashlib import sha256 +import json +import math +from numbers import Real +from pathlib import Path +import weakref + +from . import reference_sim as sim +from . import robot_profiles + + +SCHEMA = 'dvidia.arm-interface/v1' +INTERFACE_ID = 'mujoco-position-v1' +PROFILES = ('piper', 'so101', 'ur5e-2f85') +SCOPE = ('Pinned native MuJoCo position-servo interface; unloaded simulation only. ' + 'No physical robot qualification, calibrated TCP, automatic skill retargeting, ' + 'task policy, camera perception or hardware command transport.') +_BUILT = weakref.WeakKeyDictionary() + + +def _canonical(value): + return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode('utf-8') + + +def _digest(value): + return sha256(_canonical(value)).hexdigest() + + +def _profile(profile): + if type(profile) is not str or profile not in PROFILES: + raise ValueError('Profile must be piper, so101 or ur5e-2f85') + return profile + + +def _signed(value): + result = deepcopy(value) + result['contract_sha256'] = _digest(value) + return result + + +@lru_cache(maxsize=3) +def _template(profile): + """Private immutable-by-convention metadata, frozen at first description.""" + _profile(profile) + if profile in ('piper', 'so101'): + item = robot_profiles._profile(profile) + lock_path = robot_profiles.LOCK_PATH + lock = robot_profiles._lock() + files = item['files'] + arm_axes = item['arm_axes'] + joints = item['arm_joint_names'] + item['gripper_joint_names'] + units = item['native_command_units'] + joint_units = item['native_joint_position_units'] + actions = item['actuator_names'] + limits = item['native_command_range'] + dimensions = dict(item['expected_dimensions'], njnt=len(joints), ntendon=0, + nkey=1 if profile == 'piper' else 0, nmocap=0) + initial_arm = [0., 1.57, -1.3485, 0., 0., 0.] if profile == 'piper' else [0.] * arm_axes + initial_qpos = initial_arm + [0.] * len(item['gripper_joint_names']) + initial_ctrl = initial_arm + [0.] + transmissions = [{'type': 'joint', 'target': name} for name in joints[:len(actions)]] + license_claims = {item['folder']: item['simulation_asset_license']} + name, tool, tool_scope = item['name'], item['tool_frame_body'], item['tool_frame_scope'] + gripper_command = item['gripper_command'] + force_units = item['native_actuator_force_units'] + hardware_id = 'piper-sdk-planned' if profile == 'piper' else 'lerobot-so101-planned' + sdk_url = item['sdk_url'] + else: + lock_path = Path(sim.__file__).with_name('reference_assets.lock.json') + lock = sim.asset_lock() + files = sim._files(hands=False) + arm_axes = 6 + joints = list(sim.ARM_JOINTS) + [f'gripper/{side}_{part}_joint' + for side in ('right', 'left') for part in ('driver', 'coupler', 'spring_link', 'follower')] + actions = ['shoulder_pan', 'shoulder_lift', 'elbow', 'wrist_1', 'wrist_2', 'wrist_3', + 'gripper/fingers_actuator'] + units, joint_units = ['rad'] * 6 + ['native_0_255'], ['rad'] * 14 + limits = [[-6.2831, 6.2831], [-6.2831, 6.2831], [-3.1415, 3.1415], + [-6.2831, 6.2831], [-6.2831, 6.2831], [-6.2831, 6.2831], [0., 255.]] + dimensions = dict(nq=14, nv=14, nu=7, neq=3, njnt=14, ntendon=1, nkey=1, nmocap=0) + initial_arm = [-1.5708, -1.5708, 1.5708, -1.5708, -1.5708, 0.] + initial_qpos, initial_ctrl = initial_arm + [0.] * 8, initial_arm + [0.] + transmissions = [{'type': 'joint', 'target': name} for name in joints[:6]] + transmissions.append({'type': 'tendon', 'target': 'gripper/split'}) + license_claims = {key: lock['licenses'][key] for key in ('universal_robots_ur5e', 'robotiq_2f85')} + name, tool = 'UR5e + Robotiq 2F-85', 'wrist_3_link' + tool_scope = 'terminal arm link origin; not a calibrated fingertip TCP' + gripper_command = ('native position-servo input [0,255] for the coupled Robotiq tendon; ' + 'not metres, radians, force or a measured jaw gap') + force_units = ['Nm'] * 6 + ['native_tendon_generalized_force'] + hardware_id, sdk_url = 'ur-rtde-robotiq-planned', None + source_names = ('arm_interfaces.py', 'robot_profiles.py', 'reference_sim.py', 'env.py') + sources = {name: sha256(Path(__file__).with_name(name).read_bytes()).hexdigest() for name in source_names} + return { + 'schema': SCHEMA, 'id': profile, 'name': name, 'interface_id': INTERFACE_ID, + 'arm_axes': arm_axes, 'gripper_axes': 1, + 'action_names': list(actions), 'action_units': list(units), 'action_limits_native': limits, + 'action_force_units': force_units, + 'arm_action_indices': list(range(arm_axes)), 'gripper_action_indices': [arm_axes], + 'actuator_transmissions': transmissions, + 'joint_names': joints, 'joint_position_units': joint_units, + 'joint_qpos_indices': list(range(len(joints))), 'joint_dof_indices': list(range(len(joints))), + 'arm_joint_names': joints[:arm_axes], 'gripper_joint_names': joints[arm_axes:], + 'tool_frame_body': tool, 'tool_frame_scope': tool_scope, + 'gripper_command': gripper_command, 'expected_dimensions': dimensions, + 'initial_state': {'method': 'native_keyframe_0' if dimensions['nkey'] else 'native_zero_joint_state', + 'qpos_native': initial_qpos, 'ctrl_native': initial_ctrl, + 'qvel_native_per_second': [0.] * dimensions['nv'], + 'time_seconds': 0., 'hidden_settling_steps': 0}, + 'asset_lock': {'repository': lock['repository'], 'commit': lock['commit'], + 'lock_file': lock_path.name, 'lock_sha256': sha256(lock_path.read_bytes()).hexdigest(), + 'files': deepcopy(files), 'simulation_asset_licenses': license_claims}, + 'source_sha256': sources, 'runtime_binding': None, + 'scope': SCOPE, 'physical_robot_ready': False, 'task_qualified': False, + 'hardware_interfaces': [{'id': hardware_id, 'implemented': False, 'sdk_url': sdk_url}], + 'model_mutation_policy': ('Caller must not modify the built native model. Command validation checks ' + 'topology, limits and servo settings, not a complete model hash each call.'), + } + + +def contract(profile): + """Describe exact source/asset locks and command semantics without native imports.""" + return _signed(_template(_profile(profile))) + + +def catalog(): + """List the three supported simulation interfaces without NumPy or MuJoCo.""" + keys = ('id', 'name', 'arm_axes', 'gripper_axes', 'action_names', 'action_units', + 'interface_id', 'scope', 'physical_robot_ready', 'task_qualified') + return [{key: deepcopy(_template(profile)[key]) for key in keys} for profile in PROFILES] + + +def validate_contract(value): + """Check exact installed metadata and digest; a digest is integrity, not a signature. + + A runtime binding can be inspected after serialization, but command validation + additionally requires the actual model returned by build in this process. + """ + if type(value) is not dict: + raise ValueError('Contract must be an exact dictionary') + profile = _profile(value.get('id')) + base = _template(profile) + if set(value) != set(base) | {'contract_sha256'}: + raise ValueError('Contract keys differ from the installed interface') + binding = value.get('runtime_binding') + if binding is not None: + expected_keys = {'mujoco_version', 'numpy_version', 'timestep_s', 'compiled_model_sha256'} + if type(binding) is not dict or set(binding) != expected_keys: + raise ValueError('Runtime binding keys differ from the native contract') + if binding['mujoco_version'] != '3.15.0' or type(binding['numpy_version']) is not str: + raise ValueError('Runtime binding requires MuJoCo 3.15.0 and an explicit NumPy version') + sim._timestep(binding['timestep_s']) + checksum = binding['compiled_model_sha256'] + if type(checksum) is not str or len(checksum) != 64 or any(x not in '0123456789abcdef' for x in checksum): + raise ValueError('Runtime binding requires a lowercase SHA256 model digest') + expected = dict(base, runtime_binding=binding) + try: + actual_unsigned = {key: value[key] for key in base} + actual_digest = _digest(actual_unsigned) + except (TypeError, ValueError, OverflowError, RecursionError) as exc: + raise ValueError('Contract must contain finite canonical JSON data') from exc + if actual_digest != _digest(expected) or value['contract_sha256'] != actual_digest: + raise ValueError('Contract metadata/digest differs from the installed interface') + return deepcopy(value) + + +def _topology(model, expected): + """Require the scalar ordered transmission/servo topology, without mesh copying.""" + mj, np = sim._native_imports() + if not isinstance(model, mj.MjModel): + raise ValueError('Expected the native model returned by build') + dimensions = {name: int(getattr(model, name)) for name in expected['expected_dimensions']} + if dimensions != expected['expected_dimensions']: + raise ValueError('Native topology dimensions differ from the interface') + if [mj.mj_id2name(model, mj.mjtObj.mjOBJ_ACTUATOR, i) for i in range(model.nu)] != expected['action_names']: + raise ValueError('Native actuator order differs from the interface') + if [mj.mj_id2name(model, mj.mjtObj.mjOBJ_JOINT, i) for i in range(model.njnt)] != expected['joint_names']: + raise ValueError('Native joint order differs from the interface') + if not np.array_equal(model.jnt_qposadr, expected['joint_qpos_indices']) or not np.array_equal(model.jnt_dofadr, expected['joint_dof_indices']): + raise ValueError('Native scalar joint addresses differ from the interface') + joint_types = [int(mj.mjtJoint.mjJNT_SLIDE if unit == 'm' else mj.mjtJoint.mjJNT_HINGE) + for unit in expected['joint_position_units']] + if not np.array_equal(model.jnt_type, joint_types): + raise ValueError('Native joint units differ from the interface') + if not np.array_equal(model.actuator_ctrlrange, expected['action_limits_native']) or not np.all(model.actuator_ctrllimited): + raise ValueError('Native command limits differ from the interface') + for index, transmission in enumerate(expected['actuator_transmissions']): + is_joint = transmission['type'] == 'joint' + kind = mj.mjtObj.mjOBJ_JOINT if is_joint else mj.mjtObj.mjOBJ_TENDON + trn = mj.mjtTrn.mjTRN_JOINT if is_joint else mj.mjtTrn.mjTRN_TENDON + target = mj.mj_name2id(model, kind, transmission['target']) + if target < 0 or model.actuator_trntype[index] != trn or model.actuator_trnid[index].tolist() != [target, -1]: + raise ValueError('Native transmission differs from the interface') + if mj.mj_name2id(model, mj.mjtObj.mjOBJ_BODY, expected['tool_frame_body']) <= 0: + raise ValueError('Native terminal link frame is missing') + # These scalar position servos are also the only types for which the + # benchmark may derive command-equivalent feedback from actuator length. + if (not np.all(model.actuator_gaintype == mj.mjtGain.mjGAIN_FIXED) + or not np.all(model.actuator_biastype == mj.mjtBias.mjBIAS_AFFINE) + or not np.all(model.actuator_dyntype == mj.mjtDyn.mjDYN_NONE) + or not np.all(np.isfinite(model.actuator_gainprm)) + or not np.all(model.actuator_gainprm[:, 0] > 0)): + raise ValueError('Native position-servo types differ from the interface') + + +def _control_snapshot(model): + # Capture the exact mechanics controlling the command mapping and coupling. + # These arrays are small; visual meshes and inertias remain in the build hash. + names = ('actuator_gainprm', 'actuator_biasprm', 'actuator_dynprm', 'actuator_gear', + 'actuator_forcerange', 'actuator_forcelimited', 'actuator_actlimited', + 'actuator_actrange', 'jnt_range', 'jnt_limited', 'eq_type', 'eq_obj1id', + 'eq_obj2id', 'eq_data', 'eq_active0') + return {name: getattr(model, name).copy() for name in names} + + +def build(profile, assets, *, timestep=.001): + """Build the exact pinned native model and initialize it without hidden steps. + + Assets are an existing local asset root; this function does not download. + Supported native steps are 0.5, 1 and 2 ms. All seven/six commands are native + authored servo inputs, including the UR gripper's 0..255 tendon input. + """ + profile, dt = _profile(profile), sim._timestep(timestep) + metadata = contract(profile) + messages = [] + with sim.native_warnings(messages): + model = (robot_profiles.build_model(profile, assets, timestep=dt) if profile in ('piper', 'so101') + else sim.build_model(assets, timestep=dt)) + mj, np = sim._native_imports() + _topology(model, metadata) + data = mj.MjData(model) + if model.nkey: + mj.mj_resetDataKeyframe(model, data, 0) + else: + data.ctrl[:] = metadata['initial_state']['ctrl_native'] + mj.mj_forward(model, data) + if messages: + raise ValueError('Native build/initialization emitted warnings: '+str(messages)) + initial = metadata['initial_state'] + if (data.time != 0. or not np.array_equal(data.qpos, initial['qpos_native']) + or not np.array_equal(data.qvel, initial['qvel_native_per_second']) + or not np.array_equal(data.ctrl, initial['ctrl_native']) + or not all(np.isfinite(array).all() for array in (data.qpos, data.qvel, data.qacc, data.ctrl, data.xpos)) + or np.any(data.qfrc_applied) or np.any(data.xfrc_applied)): + raise ValueError('Native initialization differs from the explicit interface home state') + unsigned = {key: metadata[key] for key in _template(profile)} + unsigned['runtime_binding'] = { + 'mujoco_version': mj.__version__, 'numpy_version': np.__version__, + 'timestep_s': dt, 'compiled_model_sha256': sim.model_digest(model), + } + bound = _signed(unsigned) + _BUILT[model] = {'contract': deepcopy(bound), 'control_snapshot': _control_snapshot(model)} + validate_command(bound, model, data.ctrl) + return model, data, bound + + +def validate_command(contract_value, model, values): + """Return a fresh ordered native vector; reject invalid data without clipping. + + Requires this process's exact build-returned model and corresponding bound + contract. This function never writes controls, advances time or sends data. + One-dimensional NumPy vectors and ordinary lists/tuples of real scalars are + accepted; booleans, strings, complex numbers and coercible objects are not. + """ + if type(contract_value) is not dict or contract_value.get('runtime_binding') is None: + raise ValueError('Commands require a runtime-bound contract returned by build') + mj, np = sim._native_imports() + if not isinstance(model, mj.MjModel) or model not in _BUILT: + raise ValueError('Commands require the exact native model returned by build in this process') + registered = _BUILT[model] + # Canonical bytes distinguish booleans from numbers and reject NaN/Infinity. + try: + unchanged = _canonical(contract_value) == _canonical(registered['contract']) + except (TypeError, ValueError, OverflowError, RecursionError) as exc: + raise ValueError('Command contract must contain finite canonical JSON data') from exc + if not unchanged: + raise ValueError('Command contract differs from the model build binding') + expected = registered['contract'] + _topology(model, expected) + if model.opt.timestep != expected['runtime_binding']['timestep_s']: + raise ValueError('Native timestep differs from the model build binding') + if any(not np.array_equal(getattr(model, name), value) for name, value in registered['control_snapshot'].items()): + raise ValueError('Native servo settings or gripper coupling changed after build') + if isinstance(values, np.ndarray): + if values.ndim != 1: + raise ValueError('Command must be a one-dimensional vector') + scalars = list(values) + elif type(values) in (list, tuple): + scalars = list(values) + else: + raise ValueError('Command must be a list, tuple or one-dimensional NumPy vector') + if len(scalars) != len(expected['action_names']): + raise ValueError('Command length differs from the ordered actuator contract') + output = [] + for name, scalar, bounds in zip(expected['action_names'], scalars, expected['action_limits_native']): + if isinstance(scalar, (bool, np.bool_)) or not isinstance(scalar, Real): + raise ValueError('Command '+name+' must be a finite real scalar, excluding booleans') + try: + number = float(scalar) + except (TypeError, ValueError, OverflowError) as exc: + raise ValueError('Command '+name+' must be a finite real scalar') from exc + if not math.isfinite(number) or not bounds[0] <= number <= bounds[1]: + raise ValueError('Command '+name+' is nonfinite or outside its native range') + output.append(number) + return output + + +def named_command(contract_value, model, values): + """Validate an exact name/value dictionary and return the declared action order.""" + if type(contract_value) is not dict or type(values) is not dict: + raise ValueError('Named commands require exact contract and value dictionaries') + names = contract_value.get('action_names') + if type(names) is not list or any(type(name) is not str for name in names): + raise ValueError('Invalid action-name contract') + if set(values) != set(names) or any(type(name) is not str for name in values): + raise ValueError('Named command must supply exactly the declared actuator names') + return validate_command(contract_value, model, [values[name] for name in names]) diff --git a/src/dvidia_training/arm_skill_install.py b/src/dvidia_training/arm_skill_install.py new file mode 100644 index 0000000..2bbc3a3 --- /dev/null +++ b/src/dvidia_training/arm_skill_install.py @@ -0,0 +1,137 @@ +"""Bind a supported data-only policy to an exact local simulation adapter. + +Installation is separate from execution and never authorizes physical motion. +The first policy runner is the existing PiPER block-placement research policy; +other arms can use the shared mechanics benchmark but cannot install this policy. +""" +from __future__ import annotations + +from hashlib import sha256 +import json +from pathlib import Path + +from . import arm_interfaces +from .piper_imitation import MAX_MODEL_BYTES, validate_model +from .reference_replay import _read, _decode + +SCHEMA = 'dvidia.arm-skill-installation/v1' +INTERFACE = 'mujoco-position-v1' +RUNNER = 'piper-block-placement-v1' +RUNTIME_FILES = ( + 'arm_skill_install.py', 'arm_interfaces.py', 'piper_learned_skill.py', + 'piper_imitation.py', 'piper_imitation_score.py', 'piper_imitation_benchmark.py', + 'piper_motor_primitive.py', 'piper_task.py', 'reference_task.py', + 'reference_sim.py', 'reference_replay.py', 'robot_profiles.py', + 'robot_profiles.lock.json', 'env.py', +) + + +def _canonical(value): + return json.dumps(value, sort_keys=True, separators=(',', ':'), allow_nan=False).encode() + + +def _runtime(): + return {name: sha256(Path(__file__).with_name(name).read_bytes()).hexdigest() + for name in RUNTIME_FILES} + + +_LOADED_RUNTIME = _runtime() + + +def _checked_runtime(): + current = _runtime() + if current != _LOADED_RUNTIME: + raise ValueError('Runtime changed while loaded. Restart before installing or running.') + return current + + +def check_model(model_path, robot, *, interface=INTERFACE): + """Check policy semantics, not merely joint count. Does not compile or run.""" + profile = arm_interfaces.contract(robot) + if interface != INTERFACE: + raise ValueError('Only the local mujoco-position-v1 interface is implemented.') + if robot != 'piper': + raise ValueError('This learned policy requires PiPER. Other arms need their own trained ' + 'policy and task benchmark; matching joint counts do not transfer it.') + raw = _read(model_path, MAX_MODEL_BYTES) + model = validate_model(raw) + return { + 'compatible': True, 'runner': RUNNER, 'robot': robot, 'interface_id': interface, + 'model_sha256': sha256(raw).hexdigest(), 'model_schema': model['schema'], + 'feature_contract': model['feature_contract'], + 'profile_contract_sha256': profile['contract_sha256'], + 'control_period_s': .02, 'scope': 'simulation-only', + 'observation_origin': 'privileged_native_simulator_state', + 'physical_robot_ready': False, 'task_qualified_by_installation': False, + } + + +def _record(check, binding): + from .piper_learned_skill import TRAINED_SCOPE + record = { + 'schema': SCHEMA, 'status': 'installed_simulation_candidate', + 'compatibility': check, 'native_binding': binding, + 'runtime_sha256': _checked_runtime(), 'model_file': 'model.json', + 'trained_scope': TRAINED_SCOPE, + 'task_gate': 'Each run uses the existing bounded block scene and independent scorer. ' + 'Installation alone does not establish task success or physical readiness.', + } + record['installation_sha256'] = sha256(_canonical(record)).hexdigest() + return record + + +def install_model(model, robot, assets, output, *, interface=INTERFACE): + """Verify model/assets, then reserve a fresh installation without stepping.""" + check = check_model(model, robot, interface=interface) + raw = _read(model, MAX_MODEL_BYTES) + if sha256(raw).hexdigest() != check['model_sha256']: + raise ValueError('Model changed during compatibility check.') + _, _, binding = arm_interfaces.build(robot, assets, timestep=.001) + record = _record(check, binding) + destination = Path(output) + if destination.exists() or destination.is_symlink(): + raise ValueError('Choose a fresh installation directory; existing installations are preserved.') + destination.mkdir(parents=True, exist_ok=False) + with (destination/'model.json').open('xb') as stream: + stream.write(raw) + with (destination/'installation.json').open('xb') as stream: + stream.write(_canonical(record)+b'\n') + return record + + +def inspect_installation(directory, assets): + """Reconstruct every installed claim against current model, assets and runtime.""" + root = Path(directory) + if root.is_symlink() or not root.is_dir(): + raise ValueError('Installation must be a regular directory.') + stored = _decode(_read(root/'installation.json', 256*1024)) + if type(stored) is not dict or stored.get('schema') != SCHEMA: + raise ValueError('Unknown arm skill installation.') + compatibility = stored.get('compatibility') + if type(compatibility) is not dict: + raise ValueError('Missing policy compatibility contract.') + check = check_model(root/'model.json', compatibility.get('robot'), + interface=compatibility.get('interface_id')) + _, _, binding = arm_interfaces.build(check['robot'], assets, timestep=.001) + expected = _record(check, binding) + if _canonical(stored) != _canonical(expected): + raise ValueError('Installed model, adapter, assets or runtime changed. Install again.') + return expected + + +def run_installation(directory, assets, output, *, start_x=.30, start_y=-.03, goal=0): + """Revalidate, then dispatch only to the built-in bounded simulation runner.""" + record = inspect_installation(directory, assets) + from .piper_learned_skill import run_learned_skill + result = run_learned_skill(Path(directory)/'model.json', assets, output, + start_x=start_x, start_y=start_y, goal=goal) + link = {'schema': 'dvidia.arm-skill-run-binding/v1', + 'installation_sha256': record['installation_sha256'], + 'model_sha256': record['compatibility']['model_sha256'], + 'run_model_matches_installation': result['model_sha256'] == record['compatibility']['model_sha256'], + 'physical_robot_ready': False} + with (Path(output)/'arm-installation-binding.json').open('xb') as stream: + stream.write(_canonical(link)+b'\n') + if not link['run_model_matches_installation']: + raise ValueError('Model changed between installation inspection and run; discard this run.') + return result diff --git a/src/dvidia_training/task_feasibility.py b/src/dvidia_training/task_feasibility.py new file mode 100644 index 0000000..a98a5e1 --- /dev/null +++ b/src/dvidia_training/task_feasibility.py @@ -0,0 +1,245 @@ +"""Data-only gate for cautious, environment-conditioned manipulation planning. + +``per_finger_normal_force_budget_n`` is a calibrated allowable normal-force +ceiling per finger, accounting for both actuator limits and object damage; it +is not a force command. Probe and test-lift values are caller declarations: +there is no live probe, test-lift, or controller integration here. Mass is +never inferred from images, and this module does not qualify physical skills. +""" +from __future__ import annotations + +from collections.abc import Mapping +import json +import math + + +SCHEMA = "dvidia.task-feasibility-input/v1" +RESULT_SCHEMA = "dvidia.task-feasibility-result/v1" +MAX_INPUT_BYTES = 64 * 1024 +GRAVITY_M_S2 = 9.80665 +STATUSES = ("blocked", "probe_required", "test_lift_required", "ready_for_simulated_transport") +_FIELDS = { + "schema", "geometry_known", "reachability", "object_width_interval_m", + "gripper_max_opening_interval_m", "gripper_min_closure_m", + "mass_interval_kg", "friction_interval", + "per_finger_normal_force_budget_n", "pose_specific_payload_limit_kg", + "planned_upward_accel_m_s2", "safety_factor", "pose_uncertainty_m", + "allowed_pose_tolerance_m", "observations_fresh", "test_lift_passed", + "slip_detected", "calibration_id", +} + + +class TaskFeasibilityError(ValueError): + """Input is malformed or outside the supported conservative gate.""" + + +def _finite(value, name: str, low: float, high: float) -> float: + if type(value) not in (int, float): + raise TaskFeasibilityError(f"{name} must be a finite number in [{low}, {high}].") + try: + number = float(value) + except (OverflowError, ValueError) as exc: + raise TaskFeasibilityError(f"{name} must be a finite number in [{low}, {high}].") from exc + if not math.isfinite(number) or not low <= number <= high: + raise TaskFeasibilityError(f"{name} must be a finite number in [{low}, {high}].") + return number + + +def _interval(value, name: str, *, nullable: bool, low: float, high: float): + if value is None and nullable: + return None + if type(value) not in (list, tuple) or len(value) != 2: + raise TaskFeasibilityError(f"{name} must be a two-value interval" + (" or null." if nullable else ".")) + lower = _finite(value[0], name+" lower", low, high) + upper = _finite(value[1], name+" upper", low, high) + if lower > upper: + raise TaskFeasibilityError(f"{name} lower bound must not exceed upper bound.") + return (lower, upper) + + +def _optional_number(value, name: str, *, low: float, high: float): + return None if value is None else _finite(value, name, low, high) + + +def _validate(document: Mapping) -> dict: + if not isinstance(document, Mapping) or type(document) is not dict: + raise TaskFeasibilityError("Input must be a plain object.") + if set(document) != _FIELDS: + missing, extra = sorted(_FIELDS-set(document)), sorted(set(document)-_FIELDS) + raise TaskFeasibilityError(f"Input fields differ from schema (missing={missing}, unknown={extra}).") + if document["schema"] != SCHEMA: + raise TaskFeasibilityError("Unsupported task-feasibility schema.") + if type(document["geometry_known"]) is not bool: + raise TaskFeasibilityError("geometry_known must be boolean.") + if document["reachability"] not in ("reachable", "unreachable", "unknown"): + raise TaskFeasibilityError("reachability must be reachable, unreachable, or unknown.") + for name in ("observations_fresh", "test_lift_passed", "slip_detected"): + if type(document[name]) is not bool: + raise TaskFeasibilityError(name+" must be boolean.") + calibration = document["calibration_id"] + if calibration is not None and (type(calibration) is not str or not calibration.strip() or len(calibration) > 160): + raise TaskFeasibilityError("calibration_id must be a non-empty bounded string or null.") + + result = dict(document) + result["object_width_interval_m"] = _interval(document["object_width_interval_m"], "object_width_interval_m", nullable=False, low=1e-9, high=10.) + result["gripper_max_opening_interval_m"] = _interval(document["gripper_max_opening_interval_m"], "gripper_max_opening_interval_m", nullable=False, low=0., high=10.) + result["gripper_min_closure_m"] = _finite(document["gripper_min_closure_m"], "gripper_min_closure_m", 0., 10.) + result["mass_interval_kg"] = _interval(document["mass_interval_kg"], "mass_interval_kg", nullable=True, low=1e-9, high=1_000.) + result["friction_interval"] = _interval(document["friction_interval"], "friction_interval", nullable=True, low=0., high=10.) + result["per_finger_normal_force_budget_n"] = _optional_number(document["per_finger_normal_force_budget_n"], "per_finger_normal_force_budget_n", low=0., high=1_000_000.) + result["pose_specific_payload_limit_kg"] = _optional_number(document["pose_specific_payload_limit_kg"], "pose_specific_payload_limit_kg", low=0., high=1_000.) + result["planned_upward_accel_m_s2"] = _finite(document["planned_upward_accel_m_s2"], "planned_upward_accel_m_s2", 0., 100.) + result["safety_factor"] = _finite(document["safety_factor"], "safety_factor", 1., 100.) + result["pose_uncertainty_m"] = _finite(document["pose_uncertainty_m"], "pose_uncertainty_m", 0., 100.) + result["allowed_pose_tolerance_m"] = _finite(document["allowed_pose_tolerance_m"], "allowed_pose_tolerance_m", 0., 100.) + return result + + +def _decode(document: bytes | str) -> dict: + try: + raw = document.encode("utf-8") if type(document) is str else document + except UnicodeError as exc: + raise TaskFeasibilityError("Input must be valid UTF-8 JSON.") from exc + if not raw or len(raw) > MAX_INPUT_BYTES: + raise TaskFeasibilityError("JSON input is empty or exceeds 64 KiB.") + + def pairs(rows): + value = {} + for key, item in rows: + if key in value: + raise TaskFeasibilityError("JSON object contains a duplicate field.") + value[key] = item + return value + + def constant(token): + raise TaskFeasibilityError("JSON numbers must be finite; found " + token + ".") + + try: + value = json.loads(raw.decode("utf-8"), object_pairs_hook=pairs, parse_constant=constant) + json.dumps(value, allow_nan=False) + return value + except TaskFeasibilityError: + raise + except (ValueError, UnicodeError, RecursionError, OverflowError) as exc: + raise TaskFeasibilityError("Input must be finite UTF-8 JSON with unique keys.") from exc + + +def assess(document: dict | bytes | str) -> dict: + """Return a conservative gate result; never performs sensing or actuation.""" + if type(document) in (str, bytes): + document = _decode(document) + data = _validate(document) + reasons: list[str] = [] + required_n = friction_n = payload_limit_n = None + mass = data["mass_interval_kg"] + if mass is not None: + required_n = mass[1] * (GRAVITY_M_S2 + data["planned_upward_accel_m_s2"]) * data["safety_factor"] + friction = data["friction_interval"] + normal = data["per_finger_normal_force_budget_n"] + if friction is not None and normal is not None: + friction_n = 2. * friction[0] * normal + payload_limit = data["pose_specific_payload_limit_kg"] + if payload_limit is not None: + payload_limit_n = payload_limit * GRAVITY_M_S2 + + blocked = False + if data["reachability"] == "unreachable": + blocked = True + reasons.append("The requested pose is declared unreachable.") + if not data["observations_fresh"]: + blocked = True + reasons.append("Observations are stale; refresh the scene and robot state before reassessment.") + if data["slip_detected"]: + blocked = True + reasons.append("Slip was detected; stop transport planning and reassess the grasp.") + object_width = data["object_width_interval_m"] + grip_width = data["gripper_max_opening_interval_m"] + min_closure = data["gripper_min_closure_m"] + if object_width[0] > grip_width[1]: + blocked = True + reasons.append("The narrowest declared object width exceeds the widest declared maximum gripper opening.") + if object_width[1] < min_closure: + blocked = True + reasons.append("The widest declared object width is smaller than the gripper's minimum closure gap.") + if required_n is not None and payload_limit_n is not None and required_n > payload_limit_n: + blocked = True + reasons.append("Conservative dynamic load exceeds the calibrated pose-specific payload limit.") + if required_n is not None and friction_n is not None and required_n > friction_n: + blocked = True + reasons.append("Conservative dynamic load exceeds the two-finger friction capacity.") + + unknowns = [] + if not data["geometry_known"]: + unknowns.append("object/gripper geometry") + if data["reachability"] == "unknown": + unknowns.append("reachability") + if mass is None: + unknowns.append("object mass interval") + if friction is None: + unknowns.append("lower friction bound") + if normal is None: + unknowns.append("per-finger normal-force budget") + if payload_limit is None: + unknowns.append("pose-specific calibrated payload limit") + if data["calibration_id"] is None: + unknowns.append("calibration identity") + if object_width[1] > grip_width[0]: + unknowns.append("guaranteed fit within the uncertain maximum gripper opening") + if object_width[0] < min_closure: + unknowns.append("guaranteed object fit above the gripper minimum closure gap") + if data["pose_uncertainty_m"] > data["allowed_pose_tolerance_m"]: + unknowns.append("pose estimate within allowed tolerance") + if unknowns: + reasons.append("Measure or resolve before proceeding: " + ", ".join(unknowns) + ".") + + if blocked: + status = "blocked" + elif unknowns: + status = "probe_required" + elif not data["test_lift_passed"]: + status = "test_lift_required" + reasons.append("Run and record a cautious test lift under the same calibrated pose and load conditions before simulated transport planning.") + else: + status = "ready_for_simulated_transport" + reasons.append("Declared checks pass for simulated transport planning; this is not hardware qualification.") + return { + "schema": RESULT_SCHEMA, + "status": status, + "physical_robot_ready": False, + "observation_origin": "caller_declared_not_verified", + "reasons": reasons, + "computed": { + "conservative_required_load_n": required_n, + "two_finger_friction_capacity_n": friction_n, + "pose_specific_payload_limit_force_n": payload_limit_n, + "gravity_m_s2": GRAVITY_M_S2, + }, + "scope": ("Planning assessment only. Parallel two-finger vertical-lift approximation; ignores moment, center of mass, " + "deformation, transient impacts and unmodeled contact effects. Unknown values remain unknown; mass is never " + "inferred from images. Probe/test-lift/controller integration is not implemented. No hardware actuation " + "is performed; this is not hardware qualification."), + } + + +def template() -> dict: + """Return a synthetic example with declared values; it still requires a test lift.""" + return { + "schema": SCHEMA, + "geometry_known": True, + "reachability": "reachable", + "object_width_interval_m": [0.040, 0.044], + "gripper_max_opening_interval_m": [0.050, 0.060], + "gripper_min_closure_m": 0.010, + "mass_interval_kg": [0.180, 0.200], + "friction_interval": [0.60, 0.70], + "per_finger_normal_force_budget_n": 4.0, + "pose_specific_payload_limit_kg": 0.35, + "planned_upward_accel_m_s2": 0.50, + "safety_factor": 1.5, + "pose_uncertainty_m": 0.003, + "allowed_pose_tolerance_m": 0.010, + "observations_fresh": True, + "test_lift_passed": False, + "slip_detected": False, + "calibration_id": "synthetic-calibration-example", + } diff --git a/tests/test_arm_interface_benchmark.py b/tests/test_arm_interface_benchmark.py new file mode 100644 index 0000000..9005f82 --- /dev/null +++ b/tests/test_arm_interface_benchmark.py @@ -0,0 +1,118 @@ +import contextlib +import json +import math +from pathlib import Path +import tempfile +from types import SimpleNamespace +import unittest +from unittest.mock import patch + +from dvidia_training import arm_interface_benchmark as benchmark + + +class SettingsTests(unittest.TestCase): + def test_common_fixed_roster_and_timing_contract(self): + settings = benchmark._settings(list(benchmark.PROFILES), .4, 2) + self.assertEqual(settings["control_periods_s"], [.01, .02, .04]) + self.assertEqual(settings["physics_timestep_s"], .001) + self.assertEqual(settings["cases"], ["hold", "arm_perturbation", "gripper_cycle"]) + for period in settings["control_periods_s"]: + self.assertEqual(round(settings["duration_s"]/period)*round(period/.001), 400) + + def test_invalid_settings_rejected(self): + for duration in (True, .2, .41, math.nan, math.inf, 3.): + with self.subTest(duration=duration), self.assertRaises(ValueError): + benchmark._settings(["piper"], duration, 2) + for profiles in ([], ["unknown"], ["piper", "piper"], "piper"): + with self.subTest(profiles=profiles), self.assertRaises(ValueError): + benchmark._settings(profiles, .4, 2) + for repeats in (True, 0, 4, 1.5): + with self.subTest(repeats=repeats), self.assertRaises(ValueError): + benchmark._settings(["piper"], .4, repeats) + + +class MeasurementTests(unittest.TestCase): + def test_waveforms_preserve_units_limits_and_common_endpoints(self): + home, limits = [0., 1., 0.], [[-1., 1.], [.99, 1.01], [0., 255.]] + amplitude = benchmark._amplitudes(home, limits, [0, 1], [2]) + self.assertAlmostEqual(amplitude[0], .04) + self.assertAlmostEqual(abs(amplitude[1]), .008) + self.assertEqual(amplitude[2], 51.) + for case in benchmark.CASES: + for step in range(401): + command = benchmark._command(case, step*.001, .4, home, amplitude, [0, 1], [2]) + self.assertTrue(all(low-1e-12 <= value <= high+1e-12 for value, (low, high) in zip(command, limits))) + for elapsed in (0., .4): + command = benchmark._command(case, elapsed, .4, home, amplitude, [0, 1], [2]) + for actual, expected in zip(command, home): + self.assertAlmostEqual(actual, expected) + + def test_robotiq_input_coordinate_is_not_misreported_as_radians_or_gap(self): + gain, bias = .3137255, -100. + tendon_angle = 51.*gain/100. + self.assertAlmostEqual(benchmark._servo_equivalent(tendon_angle, gain, 0., bias), 51.) + self.assertAlmostEqual(benchmark._servo_equivalent(.007, 1000., 0., -1000.), .007) + for args in ((math.nan, 1., 0., -1.), (0., 0., 0., -1.), (0., 1., 0., 0.)): + with self.assertRaises(ValueError): + benchmark._servo_equivalent(*args) + + def test_accuracy_groups_never_mix_arm_and_gripper_units(self): + rows = [{"tracking_error": { + "arm_held_target_rad": [3., 4.], "arm_continuous_reference_rad": [0., 2.], + "gripper_held_target_native": 100., "gripper_continuous_reference_native": 50., + "terminal_position_reference_m": .001, "terminal_orientation_reference_rad": .2}}] + metrics = benchmark._metrics(rows, "native_0_255") + self.assertEqual(metrics["arm_held_target"]["unit"], "rad") + self.assertAlmostEqual(metrics["arm_held_target"]["rms"], math.sqrt(12.5)) + self.assertEqual(metrics["gripper_held_target"]["unit"], "native_0_255") + self.assertEqual(metrics["gripper_held_target"]["rms"], 100.) + self.assertEqual(metrics["terminal_position_continuous_reference"]["unit"], "m") + + def test_percentiles_are_explicit_and_empty_intervals_do_not_invent_measurements(self): + timing = benchmark._percentiles([.04, .01, .02, .03]) + self.assertEqual(timing["count"], 4) + self.assertAlmostEqual(timing["p50"], .025) + self.assertAlmostEqual(timing["p95"], .0385) + self.assertAlmostEqual(timing["p99"], .0397) + self.assertIsNone(benchmark._percentiles([])["p95"]) + + def test_failed_nonfinite_state_is_retained_without_invalid_json_numbers(self): + state = benchmark._safe_state(SimpleNamespace(time=.1, qpos=[math.nan, math.inf, -math.inf], qvel=[0.], ctrl=[1.])) + self.assertEqual(state["qpos_native"], ["nan", "inf", "-inf"]) + json.dumps(state, allow_nan=False) + + +class PreservationTests(unittest.TestCase): + def test_failed_builds_preserve_every_scheduled_attempt_without_retries(self): + from dvidia_training import reference_sim + builder = SimpleNamespace(build=lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("fixture build failure"))) + with tempfile.TemporaryDirectory() as tmp, patch.object(benchmark, "_source_snapshot", return_value=[]), \ + patch.object(benchmark, "_interfaces", return_value=builder), \ + patch.object(reference_sim, "native_warnings", side_effect=lambda _messages: contextlib.nullcontext()): + output = Path(tmp)/"study" + report = benchmark.benchmark("missing-assets", output, profiles=["piper"], duration=.4, repeats=1) + self.assertEqual(report["summary"]["attempts"], 9) + self.assertEqual(report["summary"]["failed_attempts"], 9) + self.assertEqual(len(list(output.glob("attempt-*/started.json"))), 9) + self.assertEqual(len(list(output.glob("attempt-*/result.json"))), 9) + self.assertEqual(len(list(output.glob("attempt-*/trace.json"))), 9) + for attempt in report["attempts"]: + self.assertFalse(attempt["valid"]) + self.assertEqual(attempt["error"]["stage"], "build") + self.assertEqual(attempt["trace"]["state_rows"], 0) + completion = json.loads((output/"completion.json").read_text()) + self.assertEqual(completion["recorded_attempts"], 9) + self.assertEqual(completion["valid_attempts"], 0) + with self.assertRaises(ValueError): + benchmark.benchmark("missing-assets", output, profiles=["piper"]) + + def test_invalid_asset_mapping_creates_no_partial_run(self): + with tempfile.TemporaryDirectory() as tmp: + output = Path(tmp)/"study" + with self.assertRaises(ValueError): + benchmark.benchmark({"piper": "assets"}, output) + self.assertFalse(output.exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_arm_interfaces.py b/tests/test_arm_interfaces.py new file mode 100644 index 0000000..3f5ae57 --- /dev/null +++ b/tests/test_arm_interfaces.py @@ -0,0 +1,251 @@ +"""Native actuator semantics and fail-closed command boundaries, without hardware.""" +from copy import deepcopy +from hashlib import sha256 +import json +import os +from pathlib import Path +import subprocess +import sys +import unittest + +from dvidia_training import arm_interfaces as arms + + +def resign(value): + result = deepcopy(value) + result.pop('contract_sha256', None) + result['contract_sha256'] = sha256(json.dumps(result, sort_keys=True, separators=(',', ':'), + allow_nan=False).encode()).hexdigest() + return result + + +class ArmInterfaceDescriptionTests(unittest.TestCase): + def test_catalog_has_exact_native_semantics_and_no_hardware_claim(self): + piper, so101, ur = arms.catalog() + self.assertEqual([item['id'] for item in (piper, so101, ur)], ['piper', 'so101', 'ur5e-2f85']) + self.assertEqual([item['arm_axes'] for item in (piper, so101, ur)], [6, 5, 6]) + self.assertEqual([len(item['action_names']) for item in (piper, so101, ur)], [7, 6, 7]) + self.assertEqual(piper['action_units'], ['rad'] * 6 + ['m']) + self.assertEqual(so101['action_units'], ['rad'] * 6) + self.assertEqual(ur['action_units'], ['rad'] * 6 + ['native_0_255']) + for item in (piper, so101, ur): + self.assertEqual(item['interface_id'], 'mujoco-position-v1') + self.assertFalse(item['physical_robot_ready']) + self.assertFalse(item['task_qualified']) + + def test_listing_and_description_work_without_optional_native_imports(self): + script = """ +import sys +sys.modules['mujoco'] = None +sys.modules['numpy'] = None +from dvidia_training.arm_interfaces import catalog, contract, validate_contract +assert len(catalog()) == 3 +for item in catalog(): + value = contract(item['id']) + assert validate_contract(value) == value +assert 'dvidia_training.env' not in sys.modules +""" + completed = subprocess.run([sys.executable, '-c', script], capture_output=True, text=True) + self.assertEqual(completed.returncode, 0, completed.stderr) + + def test_contract_locks_sources_selected_asset_bytes_and_licenses(self): + for profile in arms.PROFILES: + item = arms.contract(profile) + self.assertIsNone(item['runtime_binding']) + self.assertEqual(item['asset_lock']['commit'], '0059d4335f8156206f63a35662313385f7ad6d74') + self.assertEqual(item['source_sha256']['arm_interfaces.py'], sha256(Path(arms.__file__).read_bytes()).hexdigest()) + lock = item['asset_lock'] + self.assertEqual(lock['lock_sha256'], sha256(Path(arms.__file__).with_name(lock['lock_file']).read_bytes()).hexdigest()) + paths = [row['path'] for row in lock['files']] + self.assertEqual(len(paths), len(set(paths))) + self.assertTrue(all(not path.startswith('shadow_hand/') for path in paths)) + for folder in lock['simulation_asset_licenses']: + self.assertIn(folder + '/LICENSE', paths) + self.assertEqual(item['initial_state']['hidden_settling_steps'], 0) + self.assertTrue(all(not planned['implemented'] for planned in item['hardware_interfaces'])) + self.assertEqual(arms.validate_contract(item), item) + + def test_contracts_are_fresh_and_do_not_alias_cached_metadata(self): + original = arms.contract('piper') + altered = arms.contract('piper') + altered['action_names'][0] = 'changed' + altered['asset_lock']['files'][0]['sha256'] = '0' * 64 + altered['initial_state']['ctrl_native'][0] = .2 + self.assertEqual(arms.contract('piper'), original) + summary = arms.catalog() + summary[0]['action_units'][0] = 'mm' + self.assertEqual(arms.catalog()[0]['action_units'][0], 'rad') + + def test_self_consistent_digest_cannot_widen_units_limits_or_source_locks(self): + original = arms.contract('ur5e-2f85') + for key, mutate in ( + ('units', lambda value: value['action_units'].__setitem__(-1, 'm')), + ('limits', lambda value: value['action_limits_native'][-1].__setitem__(1, 256.)), + ('source', lambda value: value['source_sha256'].__setitem__('arm_interfaces.py', '0' * 64)), + ('bool', lambda value: value.__setitem__('physical_robot_ready', 0)), + ('extra', lambda value: value.__setitem__('send_to_hardware', True)), + ): + with self.subTest(key=key): + changed = deepcopy(original) + mutate(changed) + with self.assertRaises(ValueError): + arms.validate_contract(resign(changed)) + changed = deepcopy(original) + changed['contract_sha256'] = '0' * 64 + with self.assertRaises(ValueError): + arms.validate_contract(changed) + + def test_bad_profile_and_runtime_metadata_fail_before_native_build(self): + for value in ('ur5e', '', None, True, ['piper']): + with self.subTest(profile=value), self.assertRaises(ValueError): + arms.contract(value) + item = arms.contract('so101') + for binding in ({}, {'mujoco_version': 'old'}, + {'mujoco_version': '3.15.0', 'numpy_version': '2.5.3', + 'timestep_s': True, 'compiled_model_sha256': '0' * 64}): + changed = dict(item, runtime_binding=binding) + with self.assertRaises(ValueError): + arms.validate_contract(resign(changed)) + for dt in (True, .003, float('nan'), '0.001'): + with self.subTest(timestep=dt), self.assertRaises(ValueError): + arms.build('so101', 'unused', timestep=dt) + + def test_contract_describes_independent_coordinates_and_coupled_commands(self): + piper = arms.contract('piper') + so = arms.contract('so101') + ur = arms.contract('ur5e-2f85') + self.assertEqual(len(piper['joint_names']), 8) + self.assertEqual(len(piper['action_names']), 7) + self.assertEqual(piper['gripper_joint_names'], ['joint7', 'joint8']) + self.assertIn('not total jaw gap', piper['gripper_command']) + self.assertEqual(so['expected_dimensions']['neq'], 0) + self.assertIn('not linear jaw gap', so['gripper_command']) + self.assertEqual(ur['expected_dimensions']['neq'], 3) + self.assertEqual(ur['actuator_transmissions'][-1], {'type': 'tendon', 'target': 'gripper/split'}) + self.assertEqual(ur['action_limits_native'][-1], [0., 255.]) + self.assertIn('not metres', ur['gripper_command']) + self.assertIn('not a calibrated', ur['tool_frame_scope']) + + +@unittest.skipUnless(os.environ.get('DVIDIA_ROBOT_PROFILE_ASSETS') and os.environ.get('DVIDIA_REFERENCE_ASSETS'), + 'Set DVIDIA_ROBOT_PROFILE_ASSETS and DVIDIA_REFERENCE_ASSETS to pinned native assets') +class NativeArmInterfaceTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.mj, cls.np = arms.sim._native_imports() + cls.built = {} + for profile in arms.PROFILES: + assets = os.environ['DVIDIA_REFERENCE_ASSETS' if profile == 'ur5e-2f85' else 'DVIDIA_ROBOT_PROFILE_ASSETS'] + cls.built[profile] = arms.build(profile, assets) + + def test_all_native_initializations_are_explicit_unstepped_and_bound(self): + for profile, (model, data, item) in self.built.items(): + with self.subTest(profile=profile): + self.assertEqual(data.time, 0.) + self.assertEqual(data.qpos.tolist(), item['initial_state']['qpos_native']) + self.assertEqual(data.qvel.tolist(), item['initial_state']['qvel_native_per_second']) + self.assertEqual(data.ctrl.tolist(), item['initial_state']['ctrl_native']) + self.assertEqual(item['runtime_binding']['mujoco_version'], '3.15.0') + self.assertEqual(item['runtime_binding']['timestep_s'], .001) + self.assertEqual(item['runtime_binding']['compiled_model_sha256'], arms.sim.model_digest(model)) + self.assertEqual(arms.validate_contract(item), item) + self.assertEqual(arms.validate_command(item, model, data.ctrl), data.ctrl.tolist()) + self.assertEqual(int(model.nq), len(item['joint_names'])) + self.assertEqual(int(model.nu), len(item['action_names'])) + + def test_joint_and_tendon_native_transmissions_keep_their_units(self): + for profile, (model, data, item) in self.built.items(): + self.assertEqual(model.jnt_qposadr.tolist(), item['joint_qpos_indices']) + target = data.ctrl.tolist() + target[-1] = {'piper': .035, 'so101': 1., 'ur5e-2f85': 255.}[profile] + self.assertEqual(arms.validate_command(item, model, target), target) + self.assertEqual(data.time, 0.) + self.assertEqual(data.ctrl.tolist(), item['initial_state']['ctrl_native']) + ur_model = self.built['ur5e-2f85'][0] + self.assertEqual(ur_model.actuator_trntype[-1], self.mj.mjtTrn.mjTRN_TENDON) + self.assertAlmostEqual(ur_model.actuator_gainprm[-1, 0], .3137255) + self.assertEqual(ur_model.actuator_biasprm[-1, 1], -100.) + + def test_invalid_vectors_reject_without_control_or_state_mutation(self): + model, data, item = self.built['piper'] + before = (data.qpos.copy(), data.ctrl.copy(), data.time) + valid = data.ctrl.tolist() + invalid = [[], valid + [0.], [valid], '0,1,2,3,4,5,6', {name: 0. for name in item['action_names']}, + self.np.asarray([valid]), self.np.asarray(valid, dtype=complex)] + for scalar in (True, self.np.bool_(False), float('nan'), float('inf'), '0', None, 10**1000, 1j): + invalid.append([scalar] + valid[1:]) + invalid.extend([valid[:-1] + [-.00001], valid[:-1] + [.03500001]]) + for values in invalid: + with self.subTest(values=repr(values)[:100]), self.assertRaises(ValueError): + arms.validate_command(item, model, values) + self.assertTrue(self.np.array_equal(data.qpos, before[0])) + self.assertTrue(self.np.array_equal(data.ctrl, before[1])) + self.assertEqual(data.time, before[2]) + + def test_each_gripper_enforces_its_own_native_command_range(self): + for profile, (model, data, item) in self.built.items(): + command = data.ctrl.tolist() + for value in (item['action_limits_native'][-1][0] - .001, + item['action_limits_native'][-1][1] + .001): + command[-1] = value + with self.subTest(profile=profile, value=value), self.assertRaises(ValueError): + arms.validate_command(item, model, command) + + def test_named_vectors_are_exact_and_reordered_without_aliasing(self): + model, data, item = self.built['so101'] + mapping = dict(reversed(list(zip(item['action_names'], [.1, .2, .3, .4, .5, .6])))) + result = arms.named_command(item, model, mapping) + self.assertEqual(result, [.1, .2, .3, .4, .5, .6]) + result[0] = 0. + self.assertEqual(mapping['shoulder_pan'], .1) + self.assertEqual(data.ctrl.tolist(), [0.] * 6) + for invalid in ({**mapping, 'extra': 0.}, {key: value for key, value in mapping.items() if key != 'gripper'}, + {**mapping, 'gripper': True}): + with self.assertRaises(ValueError): + arms.named_command(item, model, invalid) + + def test_static_cross_profile_foreign_model_and_tampered_contract_reject(self): + model, data, item = self.built['piper'] + with self.assertRaises(ValueError): + arms.validate_command(arms.contract('piper'), model, data.ctrl) + with self.assertRaises(ValueError): + arms.validate_command(self.built['ur5e-2f85'][2], model, data.ctrl) + # Even an identical copied model was not bound by this interface build. + with self.assertRaises(ValueError): + arms.validate_command(item, deepcopy(model), data.ctrl) + changed = deepcopy(item) + changed['action_limits_native'][0][1] = 100. + with self.assertRaises(ValueError): + arms.validate_command(resign(changed), model, data.ctrl) + + def test_changed_native_limits_servo_coupling_and_timestep_fail_closed(self): + model, data, item = self.built['piper'] + for array, index, value in ((model.actuator_ctrlrange, (0, 1), 100.), + (model.actuator_gainprm, (0, 0), 81.), + (model.eq_data, (0, 1), 1.)): + saved = float(array[index]) + try: + array[index] = value + with self.assertRaises(ValueError): + arms.validate_command(item, model, data.ctrl) + finally: + array[index] = saved + old_dt = model.opt.timestep + try: + model.opt.timestep = .002 + with self.assertRaises(ValueError): + arms.validate_command(item, model, data.ctrl) + finally: + model.opt.timestep = old_dt + self.assertEqual(arms.validate_command(item, model, data.ctrl), data.ctrl.tolist()) + + def test_build_keeps_the_requested_supported_native_step(self): + for dt in (.0005, .002): + model, data, item = arms.build('so101', os.environ['DVIDIA_ROBOT_PROFILE_ASSETS'], timestep=dt) + self.assertEqual(model.opt.timestep, dt) + self.assertEqual(item['runtime_binding']['timestep_s'], dt) + self.assertEqual(data.time, 0.) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_arm_skill_install.py b/tests/test_arm_skill_install.py new file mode 100644 index 0000000..eb26718 --- /dev/null +++ b/tests/test_arm_skill_install.py @@ -0,0 +1,128 @@ +"""Wrong embodiment, changed inputs and unqualified backends fail before motion.""" +from copy import deepcopy +from hashlib import sha256 +import json +from pathlib import Path +import tempfile +import unittest +from unittest.mock import patch + +from dvidia_training import piper_imitation as imitation +from dvidia_training import arm_skill_install as install +from dvidia_training import arm_interface_cli as cli + + +class ArmSkillInstallTests(unittest.TestCase): + @classmethod + def setUpClass(cls): + rows = [{'features': [0.] * imitation.FEATURE_COUNT, + 'targets': [0.] * 6 + [.5], 'case_id': 'fixture-train'} for _ in range(16)] + cls.raw = imitation.canonical(imitation.train({'scene_ids': ['fixture-train'], 'samples': rows}, + hidden_units=8, epochs=1, batch_size=8, seed=7)) + b'\n' + + def setUp(self): + temporary = tempfile.TemporaryDirectory() + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.model = self.root/'model.json' + self.model.write_bytes(self.raw) + self.output = self.root/'installation' + self.binding = {**install.arm_interfaces.contract('piper'), + 'runtime_binding': {'test_fixture': True}} + self.build = patch.object(install.arm_interfaces, 'build', return_value=(None, None, self.binding)) + self.native = self.build.start() + self.addCleanup(self.build.stop) + + def make_install(self): + return install.install_model(self.model, 'piper', 'unused-fixture-assets', self.output) + + def test_check_accepts_existing_policy_but_never_calls_native_or_grants_task_success(self): + record = install.check_model(self.model, 'piper') + self.assertEqual(record['model_sha256'], sha256(self.raw).hexdigest()) + self.assertFalse(record['physical_robot_ready']) + self.assertFalse(record['task_qualified_by_installation']) + self.assertEqual(record['control_period_s'], .02) + self.native.assert_not_called() + + def test_same_joint_count_does_not_transfer_policy_or_accept_hardware_backend(self): + for robot, backend in [('so101', install.INTERFACE), ('ur5e-2f85', install.INTERFACE), + ('piper', 'ros2'), ('piper', 'lerobot')]: + with self.subTest(robot=robot, backend=backend), self.assertRaises(ValueError): + install.install_model(self.model, robot, 'unused', self.output, interface=backend) + self.native.assert_not_called() + self.assertFalse(self.output.exists()) + + def test_install_copies_exact_model_and_is_separate_from_execution(self): + with patch('dvidia_training.piper_learned_skill.run_learned_skill', side_effect=AssertionError('Install moved robot')): + record = self.make_install() + inspected = install.inspect_installation(self.output, 'unused-fixture-assets') + self.assertEqual(inspected, record) + self.assertEqual((self.output/'model.json').read_bytes(), self.raw) + self.assertEqual(record['status'], 'installed_simulation_candidate') + with self.assertRaises(ValueError): + self.make_install() + + def test_tampered_model_receipt_runtime_and_native_binding_reject(self): + self.make_install() + receipt_path = self.output/'installation.json' + original = receipt_path.read_bytes() + receipt = json.loads(original) + for field, replacement in [('status', 'qualified'), ('model_file', '../elsewhere'), + ('runtime_sha256', {}), ('native_binding', {})]: + changed = deepcopy(receipt) + changed[field] = replacement + # Resealing an attacker-changed receipt cannot grant compatibility. + del changed['installation_sha256'] + changed['installation_sha256'] = sha256(install._canonical(changed)).hexdigest() + receipt_path.write_bytes(install._canonical(changed)) + with self.subTest(field=field), self.assertRaises(ValueError): + install.inspect_installation(self.output, 'unused') + receipt_path.write_bytes(original) + (self.output/'model.json').write_bytes(self.raw+b' ') + with self.assertRaises(ValueError): + install.inspect_installation(self.output, 'unused') + + def test_duplicate_json_keys_and_symlink_models_are_rejected(self): + self.make_install() + path = self.output/'installation.json' + path.write_bytes(b'{"schema":"a","schema":"b"}') + with self.assertRaises(ValueError): + install.inspect_installation(self.output, 'unused') + link = self.root/'linked.json' + link.symlink_to(self.model) + with self.assertRaises(ValueError): + install.check_model(link, 'piper') + + def test_incompatible_native_assets_are_rejected_before_reserving_installation(self): + self.native.side_effect = ValueError('Asset digest mismatch') + with self.assertRaisesRegex(ValueError, 'digest'): + self.make_install() + self.assertFalse(self.output.exists()) + + def test_modified_runtime_refuses_installed_execution(self): + self.make_install() + with patch.object(install, '_runtime', return_value={}), \ + patch('dvidia_training.piper_learned_skill.run_learned_skill') as runner: + with self.assertRaisesRegex(ValueError, 'Runtime changed'): + install.run_installation(self.output, 'unused', self.root/'attempt') + runner.assert_not_called() + + def test_run_preserves_failed_attempt_and_dispatches_only_verified_copy(self): + record = self.make_install() + output = self.root/'attempt' + def run(model, assets, result, **kwargs): + self.assertEqual(model, self.output/'model.json') + self.assertEqual(kwargs, {'start_x': .309, 'start_y': -.04, 'goal': 1}) + result.mkdir() + return {'success': False, 'reason': 'dropped', 'model_sha256': sha256(self.raw).hexdigest()} + with patch('dvidia_training.piper_learned_skill.run_learned_skill', side_effect=run) as runner: + result = install.run_installation(self.output, 'unused', output, start_x=.309, start_y=-.04, goal=1) + runner.assert_called_once() + self.assertFalse(result['success']) + binding = json.loads((output/'arm-installation-binding.json').read_text()) + self.assertEqual(binding['installation_sha256'], record['installation_sha256']) + self.assertTrue(binding['run_model_matches_installation']) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_task_feasibility.py b/tests/test_task_feasibility.py new file mode 100644 index 0000000..fb36951 --- /dev/null +++ b/tests/test_task_feasibility.py @@ -0,0 +1,153 @@ +import copy +import json +import math +import unittest + +from dvidia_training.task_feasibility import ( + GRAVITY_M_S2, + RESULT_SCHEMA, + SCHEMA, + TaskFeasibilityError, + assess, + template, +) + + +class TaskFeasibilityTests(unittest.TestCase): + def test_template_is_synthetic_and_requires_test_lift(self): + result = assess(template()) + self.assertEqual(result["schema"], RESULT_SCHEMA) + self.assertEqual(result["status"], "test_lift_required") + self.assertAlmostEqual(result["computed"]["conservative_required_load_n"], + .2 * (GRAVITY_M_S2 + .5) * 1.5) + self.assertAlmostEqual(result["computed"]["two_finger_friction_capacity_n"], 4.8) + self.assertIn("not hardware qualification", result["scope"]) + self.assertFalse(result["physical_robot_ready"]) + self.assertEqual(result["observation_origin"], "caller_declared_not_verified") + + def test_ready_only_after_all_checks_and_test_lift(self): + document = template() + document["test_lift_passed"] = True + self.assertEqual(assess(document)["status"], "ready_for_simulated_transport") + + def test_unknown_mass_geometry_calibration_and_reach_require_measurement(self): + cases = ( + ("mass_interval_kg", None), + ("geometry_known", False), + ("calibration_id", None), + ("reachability", "unknown"), + ("friction_interval", None), + ("per_finger_normal_force_budget_n", None), + ("pose_specific_payload_limit_kg", None), + ) + for field, value in cases: + with self.subTest(field=field): + document = template() + document[field] = value + document["test_lift_passed"] = True + self.assertEqual(assess(document)["status"], "probe_required") + + def test_blocking_conditions_take_precedence_over_unknowns_and_test_lift(self): + cases = ( + ("observations_fresh", False), + ("slip_detected", True), + ("reachability", "unreachable"), + ) + for field, value in cases: + with self.subTest(field=field): + document = template() + document["mass_interval_kg"] = None + document[field] = value + self.assertEqual(assess(document)["status"], "blocked") + + def test_too_heavy_for_calibrated_payload_or_friction_blocks(self): + payload = template() + payload["pose_specific_payload_limit_kg"] = .01 + self.assertEqual(assess(payload)["status"], "blocked") + + grip = template() + grip["per_finger_normal_force_budget_n"] = .1 + self.assertEqual(assess(grip)["status"], "blocked") + + def test_width_fit_and_pose_uncertainty_are_conservative(self): + too_wide = template() + too_wide["object_width_interval_m"] = [.061, .065] + self.assertEqual(assess(too_wide)["status"], "blocked") + + uncertain_fit = template() + uncertain_fit["object_width_interval_m"] = [.040, .055] + self.assertEqual(assess(uncertain_fit)["status"], "probe_required") + + too_small = template() + too_small["object_width_interval_m"] = [.004, .009] + self.assertEqual(assess(too_small)["status"], "blocked") + + uncertain_closure = template() + uncertain_closure["object_width_interval_m"] = [.008, .020] + self.assertEqual(assess(uncertain_closure)["status"], "probe_required") + + uncertain_pose = template() + uncertain_pose["pose_uncertainty_m"] = .02 + self.assertEqual(assess(uncertain_pose)["status"], "probe_required") + + def test_conservative_interval_uses_upper_mass_and_lower_friction(self): + document = template() + result = assess(document) + self.assertAlmostEqual(result["computed"]["conservative_required_load_n"], + document["mass_interval_kg"][1] * + (GRAVITY_M_S2 + document["planned_upward_accel_m_s2"]) * + document["safety_factor"]) + self.assertAlmostEqual(result["computed"]["two_finger_friction_capacity_n"], + 2 * document["friction_interval"][0] * + document["per_finger_normal_force_budget_n"]) + + def test_accepts_json_and_does_not_mutate_input(self): + document = template() + original = copy.deepcopy(document) + self.assertEqual(assess(json.dumps(document))["status"], "test_lift_required") + self.assertEqual(document, original) + + def test_rejects_unknown_fields_bad_schema_and_malformed_values(self): + document = template() + document["physical_robot_ready"] = True + with self.assertRaises(TaskFeasibilityError): + assess(document) + document = template() + document["schema"] = "future" + with self.assertRaises(TaskFeasibilityError): + assess(document) + document = template() + document["mass_interval_kg"] = [0.1, math.nan] + with self.assertRaises(TaskFeasibilityError): + assess(document) + document = template() + document["safety_factor"] = .99 + with self.assertRaises(TaskFeasibilityError): + assess(document) + document = template() + document["planned_upward_accel_m_s2"] = True + with self.assertRaises(TaskFeasibilityError): + assess(document) + document = template() + document["pose_uncertainty_m"] = 10**10000 + with self.assertRaises(TaskFeasibilityError): + assess(document) + + def test_json_is_bounded_unique_finite_and_depth_errors_are_normalized(self): + with self.assertRaises(TaskFeasibilityError): + assess(b" " * (64 * 1024 + 1)) + with self.assertRaises(TaskFeasibilityError): + assess('{"schema":"x","schema":"y"}') + with self.assertRaises(TaskFeasibilityError): + assess('{"number":NaN}') + with self.assertRaises(TaskFeasibilityError): + assess('{"number":1e999}') + with self.assertRaises(TaskFeasibilityError): + assess("[" * 2000 + "0" + "]" * 2000) + + def test_exposes_input_schema_for_callers(self): + self.assertEqual(template()["schema"], SCHEMA) + + +if __name__ == "__main__": + unittest.main()