Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ src/dvidia_training/robot_profiles.lock.json text eol=lf
src/dvidia_training/camera_action_cache.py text eol=lf
src/dvidia_training/camera_action_reference_export.py text eol=lf
src/dvidia_training/camera_action_reference_model.py text eol=lf
src/dvidia_training/piper_encoder_kinematics.py text eol=lf
scripts/benchmark-piper-encoder-kinematics.py text eol=lf
src/dvidia_training/camera_action_study.py text eol=lf
src/dvidia_training/piper_rgbd_tracker.py text eol=lf
src/dvidia_training/action_demonstration.py text eol=lf
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ jobs:
- name: Test profile contracts against pinned native assets
env:
DVIDIA_ROBOT_PROFILE_ASSETS: runs/robot-profile-assets-ci
run: python -m unittest tests.test_robot_profiles tests.test_piper_task tests.test_piper_task_benchmark tests.test_piper_grip_guard tests.test_piper_grip_benchmark tests.test_piper_contact_memory tests.test_piper_contact_benchmark tests.test_piper_contact_recovery tests.test_piper_recovery_benchmark -v
run: python -m unittest tests.test_robot_profiles tests.test_piper_task tests.test_piper_task_benchmark tests.test_piper_grip_guard tests.test_piper_grip_benchmark tests.test_piper_contact_memory tests.test_piper_contact_benchmark tests.test_piper_contact_recovery tests.test_piper_recovery_benchmark tests.test_piper_encoder_kinematics -v
- name: Test reusable shoe assets and scene previews
env:
DVIDIA_ASSET_PREVIEW_TEST_ASSETS: runs/robot-profile-assets-ci
Expand Down
24 changes: 24 additions & 0 deletions benchmarks/piper-encoder-kinematics-v01/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# PiPER encoder geometry: static CPU timing

This component translates measured joint angles and a desired tool pose into an authored bounded geometric update. It does not learn, simulate a task or execute an actuator. See [the API and frame contract](../../docs/piper-encoder-kinematics-v01.md).

Two executions on November 9, 2026 are retained: the initial internal prototype probe and the subsequent portable-script verification. The module stayed unchanged; no parameters were tuned from timings. Both used 64 seeded configurations, 64 warmups per API and 1,024 timed calls per API. Targets were prepared outside the timer.

| Probe | FK median / p95 (ms) | Joint-target calculation median / p95 (ms) | Initialization (ms) |
|---|---:|---:|---:|
| Internal prototype | 0.007875 / 0.008292 | 0.030750 / 0.032410 | 220.360 |
| Portable script | 0.007750 / 0.008208 | 0.030500 / 0.032410 | 221.497 |

[Prototype receipt](prototype-timing.json) and [portable receipt](portable-timing.json) bind source and probe hashes. Only the portable probe is distributed here. Its whole-process peak RSS was 564,772,864 bytes, including Python, NumPy, asset loading and native compilation; this is not a minimum hardware requirement. The shared host was macOS 26.5.2 arm64, Python 3.12.9, NumPy 2.5.3, MuJoCo 3.15.0, with BLAS thread settings of 1.

These are static wall-clock measurements, not an observation-to-actuator loop, real-time guarantee, collision-safety result or placement benchmark. Native time remained zero; dynamics entry points were patched to refuse calls. All learned placement results and qualification statuses remain unchanged.

Run from the source checkout, using an existing pinned asset directory and a fresh receipt path:

```sh
OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 VECLIB_MAXIMUM_THREADS=1 \
PYTHONPATH=src python scripts/benchmark-piper-encoder-kinematics.py \
--assets ./robot-assets --output ./encoder-kinematics-timing.json
```

The script accepts no model or episode input and needs no renderer or network. It pins the measured source module and refuses an existing output. Platform peak RSS is reported only where its units are known (Darwin/Linux); other platforms report null. Windows and Linux execution have not been checked in this local measurement.
42 changes: 42 additions & 0 deletions benchmarks/piper-encoder-kinematics-v01/portable-timing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"api_wall_ms": {
"fk": {
"max": 0.0275,
"median": 0.00775,
"p95": 0.008208
},
"pose_action": {
"max": 0.071583,
"median": 0.0305,
"p95": 0.03240985
}
},
"configurations": 64,
"constructor_wall_ms_including_asset_validation_and_compile": 221.496958,
"host": {
"declared_blas_threads": {
"MKL_NUM_THREADS": "1",
"OMP_NUM_THREADS": "1",
"OPENBLAS_NUM_THREADS": "1",
"VECLIB_MAXIMUM_THREADS": "1"
},
"logical_cpus": 10,
"machine": "arm64",
"macos": "26.5.2",
"mujoco": "3.15.0",
"numpy": "2.5.3",
"platform": "darwin",
"python": "3.12.9",
"whole_process_peak_rss_bytes": 564772864
},
"native_time_seconds": 0.0,
"physical_robot_ready": false,
"probe_sha256": "ffe49ba49802521f32e303b91bb268b5c4a8303dbdbf247308f45284e9de9b17",
"schema": "dvidia.piper-encoder-kinematics-static-timing/v1",
"scope": "Shared-host static CPU API timing only. Targets were prepared before timing. No dynamics, rendering, learned inference, training, IO loop or hardware commands; no real-time deadline claim.",
"seed": 20261109,
"source_sha256": "6606bb8aad04e5abb1e9b6d1a8ec628a4114acd167862167184fd4eef1d21c34",
"task_qualified": false,
"timed_calls_per_api": 1024,
"warmup_calls_per_api": 64
}
41 changes: 41 additions & 0 deletions benchmarks/piper-encoder-kinematics-v01/prototype-timing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"api_wall_ms": {
"fk": {
"max": 0.028,
"median": 0.007875,
"p95": 0.008292
},
"pose_action": {
"max": 0.091959,
"median": 0.03075,
"p95": 0.03240985
}
},
"configurations": 64,
"constructor_wall_ms_including_asset_validation_and_compile": 220.359625,
"host": {
"declared_blas_threads": {
"MKL_NUM_THREADS": "1",
"OMP_NUM_THREADS": "1",
"OPENBLAS_NUM_THREADS": "1",
"VECLIB_MAXIMUM_THREADS": "1"
},
"logical_cpus": 10,
"machine": "arm64",
"macos": "26.5.2",
"mujoco": "3.15.0",
"numpy": "2.5.3",
"python": "3.12.9",
"whole_process_peak_rss_bytes": 561676288
},
"native_time_seconds": 0.0,
"physical_robot_ready": false,
"probe_sha256": "5646f69ce2b6fb0616a615c8232af196fdabfc070e062cce6ef0c105d8f64431",
"schema": "dvidia.piper-encoder-kinematics-static-timing/v1",
"scope": "Shared-host static CPU API timing only. Targets were prepared before timing. No dynamics, rendering, learned inference, training, IO loop or hardware commands; no real-time deadline claim.",
"seed": 20261109,
"source_sha256": "6606bb8aad04e5abb1e9b6d1a8ec628a4114acd167862167184fd4eef1d21c34",
"task_qualified": false,
"timed_calls_per_api": 1024,
"warmup_calls_per_api": 64
}
108 changes: 108 additions & 0 deletions docs/piper-encoder-kinematics-v01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# PiPER encoder kinematics v0.1

`PiperEncoderKinematics` converts six measured joint angles into a tool pose and
one authored differential-IK joint-target update. It owns a private robot-only
model and accepts no live task environment, object state or contact observations.
This is a geometry component for future learning integration, not a learned skill.
It is not connected to the learner, installer or physical robot control.

## Local use

Use Python 3.12 or later with the package's `arm` extra (NumPy 2.5.3 and MuJoCo
3.15.0). From a checkout containing this module, install with
`python -m pip install '.[arm]'`. Supply the existing pinned PiPER assets; if you
need to download them, `dvidia-robot-sim fetch piper --output ./robot-assets`
fetches the locked upstream allowlist into a fresh directory. The kinematics
constructor itself is offline and verifies those bytes.

```python
from dvidia_training.piper_encoder_kinematics import PiperEncoderKinematics

robot = PiperEncoderKinematics("./robot-assets")
measured_q = [0.0, 1.57, -1.3485, 0.0, 0.0, 0.0] # example encoders, radians
pose = robot.fk(measured_q)
desired_position = pose["tcp_position_world_m"].copy()
desired_position[0] += 0.005
target = robot.pose_action(
measured_q,
desired_position,
pose["tcp_quaternion_world_wxyz"],
0.035,
)
print(target) # six joint targets and one single-finger target; nothing executed
```

The last argument is **one finger's slide target in metres**, from 0 closed to
0.035 open. It is not the full jaw gap. `jacobian(measured_q)` returns a detached
6×6 spatial Jacobian in world coordinates: three linear rows followed by three
angular rows. No renderer or graphics context is needed.

## Points, frames and limits

The authored TCP is link6-local `[0, 0, 0.12003]` metres with identity local
orientation. `fk` names its world-frame position and wxyz quaternion explicitly.
It also returns `link6_position_base_m` and `link6_quaternion_base_wxyz`: the
link6 **origin** in base coordinates. These are different points. The existing
camera-action encoder feature describes that link6 origin; this module does not
change it. For the pinned fixed base (identity world transform), TCP position is
`link6_position_base_m + R(link6_quaternion_base_wxyz) @ [0, 0, 0.12003]`.
Changing a real mounting transform or fingertip calibration requires a separate
contract. Neither pose is a hardware-calibrated fingertip measurement.

Measured angles must be finite six-vectors within the native joint ranges,
including their endpoints. Even a small measured overshoot is refused rather
than clamped. This is separate from the **issued-target** interior margin of
0.02 rad. The final joint-target change stays within 0.06 rad of accepted
measurements, including native-limit endpoints. Quaternions must already be
normalized wxyz; malformed shapes, booleans, nonfinite values and invalid jaw
targets are refused.

The update matches the existing `PiperTaskEnv.pose_action`: translation error
limited to 0.015 m, rotation error to 0.05 rad, angular weight 0.25, damping
0.012, and a maximum 0.06 rad joint update before the native-limit margin.
Measured joint angles are **not previous applied servo references**. The existing
rate limiter must still run downstream; this API does not execute it.

This local geometric update does not check reachability, collision avoidance,
contact, grip, joint velocity, actuator strength or task completion. Targets are
not authorization to command hardware. One instance per worker is required
because private scratch data is reused.

## Static verification

The focused test suite passed 10 tests using the existing local MuJoCo 3.15.0
environment. It compiled static robot/full-task models, without settling,
time integration, rendering, training or task rollouts. Python dynamics entry
points were patched to fail if called.

- Independent XML transform-chain FK and the 0.12003 m tool offset matched at
17 fixed/seeded configurations, including native joint-limit endpoints.
- World-frame linear/angular Jacobians matched central finite differences at
15 interior configurations, across all six joints.
- 34 static pose-target comparisons matched the unchanged original authored
controller, using full-task snapshots with varied object/finger state only
as an independent test oracle. No such state enters the new API.
- Quaternion sign equivalence, zero-error targets, bounds/refusal behavior,
copied results and A→B→A statelessness passed.

Run these checks with existing pinned assets:

```sh
DVIDIA_ROBOT_PROFILE_ASSETS=./robot-assets PYTHONPATH=src \
python -m unittest tests.test_piper_encoder_kinematics -v
```

Without the asset variable, eight static geometry tests are explicitly skipped;
only the two interface tests run. Passing these checks establishes geometric
agreement, not improved placement success or physical qualification. The asset
lock is `4b2ee2ca675aceb2f826983cb3082a3e08e9aa0ff419b23078abcff08ebcc6fe`;
the pinned PiPER simulation assets retain their MIT license.

## Static cost

A bounded [CPU timing probe](../benchmarks/piper-encoder-kinematics-v01/README.md)
measured 0.0305 ms median and 0.0324 ms p95 for one joint-target calculation
on the recorded shared macOS host. Initialization, including asset verification
and compilation, took 221.5 ms. The receipts, earlier prototype measurement and
portable probe are retained. No dynamics, learned inference, rendering or full
control loop is included, so these numbers establish no real-time guarantee.
63 changes: 63 additions & 0 deletions scripts/benchmark-piper-encoder-kinematics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Bounded static geometry timing, with no learned model or dynamics execution."""
import argparse, inspect, json, os, platform, sys, time
from contextlib import ExitStack
from hashlib import sha256
from pathlib import Path
from unittest.mock import patch

try:
import resource
except ImportError:
resource = None

def peak_rss_bytes():
if resource is None or sys.platform not in ('darwin', 'linux'):
return None
return resource.getrusage(resource.RUSAGE_SELF).ru_maxrss * (1 if sys.platform == 'darwin' else 1024)

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--assets', required=True, help='Existing pinned PiPER asset root')
parser.add_argument('--output', required=True, help='Fresh JSON receipt path')
args = parser.parse_args()
out = Path(args.output)
if out.exists():
raise FileExistsError('Timing output must be fresh.')
import numpy as np
from dvidia_training.piper_encoder_kinematics import PiperEncoderKinematics, ARM_LIMITS_RAD
from dvidia_training import reference_sim
source = Path(inspect.getfile(PiperEncoderKinematics))
expected = "6606bb8aad04e5abb1e9b6d1a8ec628a4114acd167862167184fd4eef1d21c34"
if sha256(source.read_bytes()).hexdigest() != expected:
raise ValueError("This v0.1 probe requires the frozen encoder-kinematics source.")
mj, _ = reference_sim._native_imports()
with ExitStack() as guard:
for name in ('mj_step','mj_step1','mj_step2','mj_forward','mj_inverse','mj_collision','mj_fwdPosition'):
guard.enter_context(patch.object(mj,name,side_effect=AssertionError('No dynamics in static timing.')))
start = time.perf_counter_ns()
robot = PiperEncoderKinematics(args.assets)
constructor_ms = (time.perf_counter_ns()-start)/1e6
lower, upper = np.array(ARM_LIMITS_RAD).T
qs = np.random.default_rng(20261109).uniform(lower+.1, upper-.1, (64,6))
targets = []
for q in qs:
pose = robot.fk(q)
targets.append((np.array(pose['tcp_position_world_m'])+[.005,-.003,.004],pose['tcp_quaternion_world_wxyz']))
for q,(position,quaternion) in zip(qs,targets):
robot.fk(q); robot.pose_action(q,position,quaternion,.02)
samples = {'fk': [],'pose_action': []}
for i in range(1024):
q=qs[i%64]; position,quaternion=targets[i%64]
start=time.perf_counter_ns();robot.fk(q);samples['fk'].append((time.perf_counter_ns()-start)/1e6)
start=time.perf_counter_ns();action=robot.pose_action(q,position,quaternion,.02);samples['pose_action'].append((time.perf_counter_ns()-start)/1e6)
assert len(action['joint_targets_rad'])==6 and np.isfinite(action['joint_targets_rad']).all()
assert robot._data.time == 0.
receipt = {'schema':'dvidia.piper-encoder-kinematics-static-timing/v1','source_sha256':expected,
'probe_sha256':sha256(Path(__file__).read_bytes()).hexdigest(),'scope':'Shared-host static CPU API timing only. Targets were prepared before timing. No dynamics, rendering, learned inference, training, IO loop or hardware commands; no real-time deadline claim.',
'configurations':64,'warmup_calls_per_api':64,'timed_calls_per_api':1024,'seed':20261109,
'constructor_wall_ms_including_asset_validation_and_compile':constructor_ms,
'api_wall_ms':{name:{'median':float(np.median(values)),'p95':float(np.quantile(values,.95)),'max':float(max(values))} for name,values in samples.items()},
'host':{'machine':platform.machine(),'platform':sys.platform,'macos':platform.mac_ver()[0],'python':platform.python_version(),'numpy':np.__version__,'mujoco':mj.__version__,'logical_cpus':os.cpu_count(),'declared_blas_threads':{key:os.getenv(key) for key in ('OPENBLAS_NUM_THREADS','OMP_NUM_THREADS','MKL_NUM_THREADS','VECLIB_MAXIMUM_THREADS')},'whole_process_peak_rss_bytes':peak_rss_bytes()},
'native_time_seconds':0.,'physical_robot_ready':False,'task_qualified':False}
assert sha256(source.read_bytes()).hexdigest()==expected
with out.open('x') as f:json.dump(receipt,f,indent=2,sort_keys=True);f.write('\n')
print(json.dumps(receipt,sort_keys=True))
Loading
Loading