piper-block-placement-v01 / sources /reference_sim.py
tarzelf's picture
Publish PiPER learned placement v0.1 evidence
10edc19 verified
Raw History Blame Contribute Delete
24.3 kB
"""Pinned hardware reference mechanics; no physical controller or skill policy.
Asset installation is an explicit network operation. All later work is local.
Imports of the optional native simulator are deferred until a model is requested.
"""
from __future__ import annotations
from contextlib import contextmanager
from hashlib import sha256
import json
import math
import os
from pathlib import Path
import platform
import statistics
import stat
import time
from urllib.request import Request, urlopen
PROFILE = "ur5e-2f85-menagerie-reference-v0.1"
COMMIT = "0059d4335f8156206f63a35662313385f7ad6d74"
ARM_JOINTS = ("shoulder_pan_joint", "shoulder_lift_joint", "elbow_joint",
"wrist_1_joint", "wrist_2_joint", "wrist_3_joint")
ARM_BODIES = ("shoulder_link", "upper_arm_link", "forearm_link",
"wrist_1_link", "wrist_2_link", "wrist_3_link")
CONTROL_DT = .02
CASES = ("hold_open", "arm_sweep", "jaw_cycle")
SCOPE = ("Pinned reference geometry, inertias, collision proxies and coupled gripper. "
"Authored native servos, privileged state and synthetic commands. "
"No serial-specific calibration, measured hardware response, task policy or physical qualification.")
def asset_lock():
return json.loads(Path(__file__).with_name("reference_assets.lock.json").read_text(encoding="utf-8"))
def _files(hands=False):
return [x for x in asset_lock()["files"] if hands or not x["path"].startswith("shadow_hand/")]
def _regular_bytes(path, size):
before = path.lstat()
if not stat.S_ISREG(before.st_mode):
raise ValueError(f"Expected a regular non-symlink asset: {path.name}")
if before.st_size != size:
raise ValueError(f"Asset size mismatch: {path.name}")
flags = os.O_RDONLY | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_BINARY", 0)
descriptor = os.open(path, flags)
with os.fdopen(descriptor, "rb") as stream:
opened = os.fstat(stream.fileno())
if not stat.S_ISREG(opened.st_mode) or opened.st_size != size:
raise ValueError("Asset must remain a bounded regular file during opening")
if before.st_ino and opened.st_ino and (before.st_dev, before.st_ino) != (opened.st_dev, opened.st_ino):
raise ValueError("Asset file identity changed during opening")
content = stream.read(size + 1)
if len(content) != size:
raise ValueError(f"Asset size mismatch: {path.name}")
return content
def _asset_payload(directory, *, hands=False):
root = Path(directory)
if root.is_symlink() or not root.is_dir():
raise ValueError("Asset root must be an existing non-symlink directory")
records = _files(hands)
blobs = {}
for record in records:
path = root / record["path"]
# Reject redirected subdirectories before allowing the native parser access.
if any(x.is_symlink() for x in (path.parent, path.parent.parent)):
raise ValueError("Asset subdirectories must not be symlinks")
content = _regular_bytes(path, record["bytes"])
if sha256(content).hexdigest() != record["sha256"]:
raise ValueError(f"Asset digest mismatch: {record['path']}")
blobs[record["path"]] = content
metadata = {"commit": COMMIT, "files_verified": len(records),
"bytes_verified": sum(x["bytes"] for x in records),
"lock_sha256": sha256(Path(__file__).with_name("reference_assets.lock.json").read_bytes()).hexdigest(),
"licenses": {k: v for k, v in asset_lock()["licenses"].items()
if hands or k != "shadow_hand"}}
return metadata, blobs
def verify_assets(directory, *, hands=False):
return _asset_payload(directory, hands=hands)[0]
def _spec(blobs, folder, filename):
mj, _ = _native_imports()
prefix = folder + "/"
local = {key[len(prefix):]: content for key, content in blobs.items() if key.startswith(prefix)}
# The native parser receives the exact verified bytes in memory. It does not
# reread model/mesh/include paths between verification and compilation.
return mj.MjSpec.from_string(local[filename].decode("utf-8"),
include={key: content for key, content in local.items() if key.endswith(".xml")},
assets={key: content for key, content in local.items() if key.startswith("assets/")})
def fetch_assets(directory, *, hands=False):
"""Download only the packaged, pinned allowlist, retaining upstream licenses.
A failed download leaves an incomplete fresh directory for inspection; verify
rejects it. No credentials, account, dynamic manifest or arbitrary URL is used.
"""
destination = Path(directory)
destination.mkdir(parents=True, exist_ok=False)
for record in _files(hands):
url = f"https://raw.githubusercontent.com/google-deepmind/mujoco_menagerie/{COMMIT}/{record['path']}"
request = Request(url, headers={"User-Agent": "DVIDIA-Reference-Simulator/0.1"})
with urlopen(request, timeout=30) as response:
if not response.geturl().startswith("https://raw.githubusercontent.com/"):
raise ValueError("Asset download redirected outside the pinned source host")
content = response.read(record["bytes"] + 1)
if len(content) != record["bytes"] or sha256(content).hexdigest() != record["sha256"]:
raise ValueError(f"Downloaded asset does not match the lock: {record['path']}")
path = destination / record["path"]
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("xb") as stream:
stream.write(content)
return verify_assets(destination, hands=hands)
def _native_imports():
try:
import mujoco
import numpy as np
except ImportError as exc:
raise RuntimeError("Install dvidia-training with the optional arm extra") from exc
if mujoco.__version__ != "3.15.0":
raise RuntimeError("Reference simulation requires MuJoCo 3.15.0")
return mujoco, np
@contextmanager
def native_warnings(messages):
from .env import _NATIVE_LOCK
mj, _ = _native_imports()
with _NATIVE_LOCK:
previous = mj.get_mju_user_warning()
mj.set_mju_user_warning(lambda value: messages.append(str(value)))
try:
yield
finally:
mj.set_mju_user_warning(previous)
def _timestep(value):
if isinstance(value, bool) or not isinstance(value, (int, float)) or value not in (.002, .001, .0005):
raise ValueError("Reference timestep must be 0.002, 0.001 or 0.0005 seconds")
return float(value)
def validate_duration(seconds):
if isinstance(seconds, bool) or not isinstance(seconds, (int, float)) or not math.isfinite(seconds) or not .1 <= seconds <= 10:
raise ValueError("Reference duration must be finite, between 0.1 and 10 seconds")
if not math.isclose(seconds / CONTROL_DT, round(seconds / CONTROL_DT), rel_tol=0, abs_tol=1e-9):
raise ValueError("Duration must be an integer number of 20 ms boundaries")
return float(seconds)
def build_model(directory, *, timestep=.001, probe_position=None):
_, blobs = _asset_payload(directory)
mj, _ = _native_imports()
arm = _spec(blobs, "universal_robots_ur5e", "ur5e.xml")
jaw = _spec(blobs, "robotiq_2f85", "2f85.xml")
# Resolve child/parent option differences explicitly, preserving the jaw's
# elliptic contact cone and impratio; do not silently accept attach warnings.
arm.option.cone = jaw.option.cone
arm.option.impratio = jaw.option.impratio
arm.option.timestep = jaw.option.timestep = _timestep(timestep)
arm.option.integrator = jaw.option.integrator = mj.mjtIntegrator.mjINT_IMPLICITFAST
arm.attach(jaw, prefix="gripper/", site="attachment_site")
arm.worldbody.add_geom(name="reference_floor", type=mj.mjtGeom.mjGEOM_PLANE,
size=[2, 2, .1], rgba=[.9, .9, .88, 1])
if probe_position is not None:
if len(probe_position) != 3 or not all(math.isfinite(x) for x in probe_position):
raise ValueError("Probe requires three finite coordinates")
arm.worldbody.add_geom(name="collision_probe", type=mj.mjtGeom.mjGEOM_SPHERE,
pos=probe_position, size=[.025, 0, 0], rgba=[1, .4, .2, 1])
return arm.compile()
def model_digest(model):
mj, np = _native_imports()
buffer = np.empty(mj.mj_sizeModel(model), dtype=np.uint8)
mj.mj_saveModel(model, buffer=buffer)
return sha256(buffer.tobytes()).hexdigest()
def _name(model, kind, index):
mj, _ = _native_imports()
return mj.mj_id2name(model, kind, int(index)) or f"unnamed_{index}"
def model_inventory(model):
mj, np = _native_imports()
active = (model.geom_contype != 0) | (model.geom_conaffinity != 0)
bodies = []
for i in range(1, model.nbody):
geoms = np.flatnonzero(model.geom_bodyid == i)
bodies.append({"name": _name(model, mj.mjtObj.mjOBJ_BODY, i),
"mass_kg": float(model.body_mass[i]),
"inertia_kg_m2": model.body_inertia[i].tolist(),
"collision_geoms": int(np.count_nonzero(active[geoms])),
"visual_geoms": int(np.count_nonzero(~active[geoms]))})
joints = [{"name": _name(model, mj.mjtObj.mjOBJ_JOINT, i),
"type": int(model.jnt_type[i]), "limited": bool(model.jnt_limited[i]),
"range": model.jnt_range[i].tolist(),
"qpos_address": int(model.jnt_qposadr[i]), "velocity_address": int(model.jnt_dofadr[i])}
for i in range(model.njnt)]
actuators = []
for i in range(model.nu):
trn = int(model.actuator_trntype[i])
kind = mj.mjtObj.mjOBJ_TENDON if trn == int(mj.mjtTrn.mjTRN_TENDON) else mj.mjtObj.mjOBJ_JOINT
actuators.append({"name": _name(model, mj.mjtObj.mjOBJ_ACTUATOR, i),
"transmission_type": trn,
"target": _name(model, kind, model.actuator_trnid[i, 0]),
"control_range": model.actuator_ctrlrange[i].tolist(),
"control_limited": bool(model.actuator_ctrllimited[i]),
"native_force_range": model.actuator_forcerange[i].tolist(),
"force_limited": bool(model.actuator_forcelimited[i])})
return {"joint_coordinates_nq": model.nq, "velocity_dofs_nv": model.nv,
"joints": joints, "actuator_count": model.nu, "actuators": actuators,
"bodies": bodies, "equality_constraints": model.neq, "tendons": model.ntendon,
"contact_exclusions": model.nexclude, "native_sensor_definitions": model.nsensor,
"native_sensor_channels": model.nsensordata,
"collision_geom_count": int(np.count_nonzero(active)),
"visual_geom_count": int(np.count_nonzero(~active)),
"compiled_model_sha256": model_digest(model)}
def audit_reference(directory, *, hands=False):
provenance, blobs = _asset_payload(directory, hands=hands)
mj, _ = _native_imports()
warnings = []
with native_warnings(warnings):
model = build_model(directory)
inventory = model_inventory(model)
if model.nq != 14 or model.nu != 7 or model.neq != 3 or model.ntendon != 1:
raise ValueError("Pinned reference composition changed its mechanical contract")
by_name = {x["name"]: x for x in inventory["bodies"]}
coverage = {name: by_name[name]["collision_geoms"] for name in ARM_BODIES}
hand_models = {}
if hands:
for side in ("left", "right"):
hand = _spec(blobs, "shadow_hand", f"{side}_hand.xml").compile()
hand_models[side] = model_inventory(hand)
return {"schema": "dvidia.reference-audit/v1", "profile": PROFILE, "scope": SCOPE,
"physical_robot_ready": False, "asset_provenance": provenance,
"settings": {"timestep_seconds": .001, "integrator": "implicitfast",
"contact_cone": "elliptic", "impratio": 10,
"mount": "upstream attachment_site, gripper namespace, no added adapter mass",
"tcp": "upstream gripper/pinch; not a measured physical TCP"},
"inventory": inventory, "arm_link_collision_coverage": coverage,
"fixed_base_collision_geoms": by_name["base"]["collision_geoms"],
"arm_command_unit": "joint target radians",
"gripper_command_unit": "native 0–255 servo input; not meters or calibrated physical force",
"hand_models": hand_models, "bimanual_assembly_implemented": False,
"calibration_gates": {name: "unmeasured" for name in
("serial_kinematics", "base_and_tcp_frames", "actuator_response_and_latency",
"gripper_encoder_gap_hysteresis", "contact_and_slip", "deployed_sensor_age",
"physical_task_success")}, "warnings": warnings}
def _initial(model):
mj, _ = _native_imports()
data = mj.MjData(model)
mj.mj_resetDataKeyframe(model, data, 0)
mj.mj_forward(model, data)
return data
def _sample(model, data, elapsed, command):
mj, np = _native_imports()
ids = [mj.mj_name2id(model, mj.mjtObj.mjOBJ_JOINT, name) for name in ARM_JOINTS]
site = mj.mj_name2id(model, mj.mjtObj.mjOBJ_SITE, "gripper/pinch")
pads = [mj.mj_name2id(model, mj.mjtObj.mjOBJ_GEOM, f"gripper/{side}_pad1") for side in ("left", "right")]
quat = np.empty(4)
mj.mju_mat2Quat(quat, data.site_xmat[site])
return {"t_seconds": elapsed, "joint_position_rad": data.qpos[model.jnt_qposadr[ids]].tolist(),
"joint_velocity_rad_s": data.qvel[model.jnt_dofadr[ids]].tolist(),
"linkage_position_rad": data.qpos[6:].tolist(),
"tcp_position_m": data.site_xpos[site].tolist(), "tcp_quaternion_wxyz": quat.tolist(),
"pad_center_separation_m": float(np.linalg.norm(data.geom_xpos[pads[0]] - data.geom_xpos[pads[1]])),
"arm_points_m": [data.xpos[mj.mj_name2id(model, mj.mjtObj.mjOBJ_BODY, name)].tolist()
for name in ("base",) + ARM_BODIES] + [data.site_xpos[site].tolist()],
"pad_centers_m": [data.geom_xpos[index].tolist() for index in pads],
"contact_count": data.ncon,
"issued_joint_targets_rad": command[:6].tolist(), "issued_gripper_0_255": float(command[6])}
def _summary(values):
ordered = sorted(values)
return {"count": len(values), "median": statistics.median(ordered),
"p95": ordered[math.ceil(.95 * len(ordered)) - 1], "max": ordered[-1]}
def _command(case, elapsed, home):
_, np = _native_imports()
result = np.array(home, dtype=float, copy=True)
if case == "arm_sweep":
result[0] += .20 * math.sin(math.pi * elapsed)
elif case == "jaw_cycle":
result[6] = 127.5 * (1 - math.cos(math.pi * elapsed))
elif case != "hold_open":
raise ValueError("Unknown reference motion case")
return result
def run_case(directory, case, *, timestep=.001, seconds=4.):
mj, np = _native_imports()
timestep = _timestep(timestep)
seconds = validate_duration(seconds)
if case not in CASES:
raise ValueError("Unknown reference motion case")
warnings = []
ticks, chunks, trajectory = [], [], []
with native_warnings(warnings):
started = time.perf_counter()
model = build_model(directory, timestep=timestep)
compile_seconds = time.perf_counter() - started
data = _initial(model)
home = model.key_ctrl[0].copy()
# Settling is outside the reported physics throughput/tick timing.
for _ in range(round(1 / timestep)):
data.ctrl[:] = home
mj.mj_step(model, data)
mj.mj_forward(model, data)
trajectory.append(_sample(model, data, 0., home))
physics_wall = 0.
for boundary in range(round(seconds / CONTROL_DT)):
elapsed = boundary * CONTROL_DT
command = _command(case, elapsed, home)
data.ctrl[:] = command
start = time.perf_counter_ns()
for _ in range(round(CONTROL_DT / timestep)):
before = time.perf_counter_ns()
mj.mj_step(model, data)
ticks.append((time.perf_counter_ns() - before) / 1e6)
mj.mj_forward(model, data)
chunk_ms = (time.perf_counter_ns() - start) / 1e6
physics_wall += chunk_ms / 1000
chunks.append(chunk_ms)
trajectory.append(_sample(model, data, (boundary + 1) * CONTROL_DT, command))
valid = bool(not warnings and not any(x.number for x in data.warning)
and np.isfinite(data.qpos).all() and np.isfinite(data.qvel).all()
and np.isfinite(data.qacc).all() and np.isfinite(data.site_xpos).all()
and np.isfinite(data.actuator_force).all()
and all(math.isfinite(v) for row in trajectory for k in
("joint_position_rad", "joint_velocity_rad_s", "tcp_position_m", "linkage_position_rad") for v in row[k]))
nonfinite_trajectory_omitted = False
try:
json.dumps(trajectory, allow_nan=False)
except ValueError:
trajectory = []
valid = False
nonfinite_trajectory_omitted = True
return {"case": case, "valid": valid, "warnings": warnings,
"nonfinite_trajectory_omitted": nonfinite_trajectory_omitted,
"compiled_model_sha256": model_digest(model), "timestep_seconds": timestep,
"control_dt_seconds": CONTROL_DT, "simulated_seconds": seconds,
"compile_and_asset_verification_seconds": compile_seconds,
"physics_chunks_wall_seconds": physics_wall,
"simulation_seconds_per_wall_second": seconds / physics_wall,
"native_tick_ms": _summary(ticks), "control_chunk_ms": _summary(chunks),
"timing_scope": "mj_step calls with per-tick instrumentation; chunks also include boundary mj_forward. "
"Excludes verification/compile, settling, sampling, rendering, perception, policy, transport and real actuation.",
"trajectory": trajectory}
def collision_probe(directory):
"""Matched overlap diagnostic, not a path planner or collision safety test."""
mj, np = _native_imports()
warnings = []
attempts = []
with native_warnings(warnings):
model = build_model(directory)
data = _initial(model)
body = mj.mj_name2id(model, mj.mjtObj.mjOBJ_BODY, "upper_arm_link")
active = (model.geom_contype != 0) | (model.geom_conaffinity != 0)
geom = int(np.flatnonzero((model.geom_bodyid == body) & active)[-1])
position = data.geom_xpos[geom].copy()
for mask in (False, True):
model = build_model(directory, probe_position=position.tolist())
if mask:
bodies = [mj.mj_name2id(model, mj.mjtObj.mjOBJ_BODY, name) for name in ARM_BODIES]
for index in np.flatnonzero(np.isin(model.geom_bodyid, bodies)):
model.geom_contype[index] = model.geom_conaffinity[index] = 0
data = _initial(model)
probe = mj.mj_name2id(model, mj.mjtObj.mjOBJ_GEOM, "collision_probe")
pairs = []
for contact in data.contact[:data.ncon]:
if probe in (contact.geom1, contact.geom2):
other = contact.geom2 if contact.geom1 == probe else contact.geom1
pairs.append({"other_body": _name(model, mj.mjtObj.mjOBJ_BODY, model.geom_bodyid[other]),
"signed_distance_m": float(contact.dist)})
attempts.append({"arm_collision_masks_disabled": mask, "probe_contact_count": len(pairs), "pairs": pairs})
return {"scope": "Authored initial sphere overlap at an upper-arm collision proxy; no motion or real collision trial",
"probe_position_m": position.tolist(), "attempts": attempts, "warnings": warnings}
def benchmark_reference(directory, *, repeats=2, seconds=4., hands=False):
if isinstance(repeats, bool) or not isinstance(repeats, int) or not 1 <= repeats <= 5:
raise ValueError("Repeats must be an integer between 1 and 5")
seconds = validate_duration(seconds)
audit = audit_reference(directory, hands=hands)
attempts = []
for case in CASES:
for timestep in (.002, .001, .0005):
for repeat in range(repeats):
try:
attempt = run_case(directory, case, timestep=timestep, seconds=seconds)
except Exception as exc:
attempt = {"case": case, "timestep_seconds": timestep, "valid": False,
"error_type": type(exc).__name__,
"error": str(exc).replace(str(Path(directory).resolve()), "[assets]")[:500]}
attempt["repeat"] = repeat
attempts.append(attempt)
convergence = []
_, np = _native_imports()
for case in CASES:
for coarse in (.002, .001):
for repeat in range(repeats):
pair = [next(x for x in attempts if x["case"] == case and x["timestep_seconds"] == dt and x["repeat"] == repeat)
for dt in (coarse, coarse / 2)]
result = {"case": case, "repeat": repeat, "timestep_pair_seconds": [coarse, coarse / 2],
"valid": all(x["valid"] for x in pair)}
if result["valid"]:
rows = [x["trajectory"] for x in pair]
def differences(key):
return np.asarray([x[key] for x in rows[0]]) - np.asarray([x[key] for x in rows[1]])
dq = differences("joint_position_rad")
dx = differences("tcp_position_m")
dg = differences("pad_center_separation_m")
result.update({"matched_boundaries": len(rows[0]), "joint_position_rmse_rad": float(np.sqrt(np.mean(dq ** 2))),
"max_joint_position_difference_rad": float(np.max(np.abs(dq))),
"tcp_distance_rmse_m": float(np.sqrt(np.mean(np.sum(dx ** 2, axis=1)))),
"max_tcp_distance_m": float(np.max(np.linalg.norm(dx, axis=1))),
"pad_center_separation_rmse_m": float(np.sqrt(np.mean(dg ** 2))),
"max_pad_center_separation_difference_m": float(np.max(np.abs(dg)))})
convergence.append(result)
source_files = ("reference_sim.py", "reference_assets.lock.json", "embodiment_cli.py", "reference_report.html")
return {"schema": "dvidia.reference-benchmark/v1", "profile": PROFILE, "scope": SCOPE,
"physical_robot_ready": False, "task_success_evaluated": False,
"hardware": {"platform": platform.platform(), "machine": platform.machine(),
"python": platform.python_version(), "mujoco": "3.15.0", "gpu_used": False},
"protocol": {"cases": list(CASES), "timesteps_seconds": [.002, .001, .0005],
"repeats": repeats, "seconds_per_case": seconds, "control_dt_seconds": CONTROL_DT,
"same_synthetic_scene_and_commands": True,
"repeat_scope": "Repeated deterministic numerical/performance diagnostics, not independent physical or skill trials",
"pad_measurement": "Euclidean distance between upstream left_pad1/right_pad1 geom centers; not calibrated inner jaw gap",
"convergence_scope": "Matched 20 ms boundaries including post-settling start; adjacent timestep comparison, not comparison with reality"},
"implementation_sha256": {name: sha256(Path(__file__).with_name(name).read_bytes()).hexdigest() for name in source_files},
"audit": audit, "collision_probe": collision_probe(directory),
"attempted": len(attempts), "valid_attempts": sum(x["valid"] for x in attempts),
"attempts": attempts, "convergence": convergence}