Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
33ea11c
runtime draft
maxxgx May 15, 2026
8f2b00a
add tests
maxxgx May 15, 2026
ab1bb31
add goal time to send action
maxxgx May 17, 2026
07790a7
remove plan
maxxgx May 17, 2026
c79b0e9
fix style
maxxgx May 17, 2026
e6a9863
remove outdated example
maxxgx May 17, 2026
4368f33
add example script
maxxgx May 19, 2026
ed9fda7
replace ActionCursor with deque, return 1D and 2D shape for select_ac…
maxxgx May 19, 2026
cdb3fab
remove obs_to_input, move inside policy runtime
maxxgx May 19, 2026
41003bb
Address comment: runtime manages robot/camera lifecycle
maxxgx May 19, 2026
93696bb
Potential fix for pull request finding
maxxgx May 19, 2026
90effbf
Potential fix for pull request finding
maxxgx May 19, 2026
ac6c8e3
address copilot
maxxgx May 19, 2026
211fba4
fix ruff
maxxgx May 19, 2026
3d05530
fix all linting issues
maxxgx May 19, 2026
51a8927
update copyright header
maxxgx May 20, 2026
2c47986
feat(runtime): cli, telemetry (zenoh), fault tolerance
maxxgx May 17, 2026
566fd0c
rename
maxxgx May 17, 2026
d8a5d07
Refactor telemetry: add callbacks
maxxgx May 18, 2026
df3cc20
minors fixes
maxxgx May 18, 2026
96247fb
minor fixes 2
maxxgx May 18, 2026
ae2a3fa
remove CLI (defered)
maxxgx May 19, 2026
061514e
Rerun poc
maxxgx May 19, 2026
1cc56fe
Rerun poc pt2
maxxgx May 19, 2026
9857c07
finish rebase
maxxgx May 19, 2026
508dd23
rerun connect
maxxgx May 19, 2026
992dfdf
fix rerun
maxxgx May 19, 2026
1b4d633
more rerun options
maxxgx May 20, 2026
4507ee8
rerun: viz
maxxgx May 20, 2026
159c39e
rerun: update viz
maxxgx May 20, 2026
2153a8c
rerun: update viz
maxxgx May 20, 2026
b6f88f8
rerun: update viz
maxxgx May 20, 2026
91a796a
rerun: update viz
maxxgx May 20, 2026
4431a89
remove zenoh callback (will add it future PR)
maxxgx May 20, 2026
93578b1
update pyproject
maxxgx May 19, 2026
1e45ec5
fix merge
maxxgx May 20, 2026
9b93aec
simplify example
maxxgx May 20, 2026
e8c8670
fix tests
maxxgx May 20, 2026
3c8b0a3
address PR comments
maxxgx May 21, 2026
f2df028
fix style
maxxgx May 21, 2026
fb12417
fix style
maxxgx May 21, 2026
abdd0a0
Merge branch 'max/runtime' into max/runtime-cli-telemetry
maxxgx May 21, 2026
7f3f558
Merge branch 'main' into max/runtime-cli-telemetry
maxxgx May 21, 2026
40b30b5
update example doc
maxxgx May 21, 2026
da39af4
finalize rerun viz
maxxgx May 21, 2026
9986ab7
fix docstring
maxxgx May 21, 2026
b717bd7
improve execution/merging
maxxgx May 22, 2026
e13a0f3
add gif
maxxgx May 22, 2026
e6fd759
gif speed x2
maxxgx May 22, 2026
633b3a1
move gif before installation
maxxgx May 22, 2026
74b6d34
add separator
maxxgx May 22, 2026
64c4101
fix trossen obs/action shape
maxxgx May 25, 2026
e2fdba6
add task, direct camera api, sync inference script
maxxgx May 25, 2026
1492904
sync inference script
maxxgx May 25, 2026
a9075d3
fix action queue offset for async
maxxgx May 25, 2026
b2ec0e8
default lerp = 3
maxxgx May 25, 2026
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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ Physical AI Runtime provides the deployment-side components for running trained
- **Inference Engine** — Load exported policies from Studio with auto-detected backends
- **Policy Runtime** — Control loop with observation building and action dispatch

---

<p align="center">
<img src="docs/assets/inference_rerun.gif" alt="Inference demo" width="100%">
</p>

## Installation

```bash
Expand Down
Binary file added docs/assets/inference_rerun.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
205 changes: 137 additions & 68 deletions examples/runtime/async_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,102 +2,171 @@
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Async inference with PolicyRuntime.

python examples/runtime/async_inference.py \
--model ./exports/pi05_cans_openvino \
--device GPU.0 \
--port /dev/ttyACM0 \
--calibration /home/max/.cache/physicalai/robots/a8d8d997-a59e-4423-9006-5d991d223887/calibrations/0b2f185a-8ab2-4956-91c2-3a2ac2dbd8c1.json \
--overhead-camera /dev/v4l/by-id/usb-UGREEN_Camera_2K_UGREEN_Camera_2K_SN0001-video-index0 \
--arm-camera 353322271391 \
--front-camera /dev/v4l/by-id/usb-Innomaker_Innomaker-U20CAM-1080p-S1_SN0001-video-index0 \
--width 640 \
--height 480 \
--fps 30 \
--duration-s 60
"""Run a trained policy on hardware with real-time Rerun visualization.

Prerequisites::

uv sync --extra capture --extra robots --extra observer-rerun

Examples:

# SO101 with 3 cameras, Rerun viewer auto-launched
python examples/runtime/async_inference.py \\
--robot so101 --port /dev/ttyACM0 --calibration ./cal.json \\
--model ./exports/my_model \\
--camera overhead:uvc:/dev/video0 \\
--camera arm:realsense:353322271391 \\
--camera front:uvc:/dev/video2 \\
--rerun spawn

# Trossen WidowXAI
python examples/runtime/async_inference.py \\
--robot widowxai --ip 192.168.1.2 \\
--model ./exports/my_model \\
--camera front:uvc:/dev/video0

# Bimanual Trossen WidowXAI
python examples/runtime/async_inference.py \\
--robot bimanual_widowxai --ip-left 192.168.1.2 --ip-right 192.168.1.3 \\
--model ./exports/my_model

# No --camera args → interactive selection
python examples/runtime/async_inference.py \\
--robot so101 --port /dev/ttyACM0 --calibration ./cal.json \\
--model ./exports/my_model --rerun spawn
"""

from __future__ import annotations

import argparse
import os
import signal

import openvino as ov
import numpy as np

from physicalai.capture import discover_all
from physicalai.capture.transport import SharedCamera
from physicalai.capture import select_cameras_interactive
from physicalai.inference import InferenceModel
from physicalai.robot import SO101
from physicalai.runtime import (
ActionQueue,
AsyncExecution,
LerpSmoother,
PolicyRuntime,
RerunCallback,
)

from utils import build_robot, parse_camera_specs


def main() -> None:
# Force-exit on second Ctrl+C (Rerun's blocked channels prevent clean shutdown)
def _handle_sigint(sig: int, frame: object) -> None:
# Restore default handler so next Ctrl+C kills immediately via OS signal
signal.signal(signal.SIGINT, signal.SIG_DFL)
print("\nInterrupting... press Ctrl+C again to force kill.")
raise KeyboardInterrupt

signal.signal(signal.SIGINT, _handle_sigint)

parser = argparse.ArgumentParser(
description="Run a trained policy on hardware",
formatter_class=argparse.RawDescriptionHelpFormatter,
)

# Robot
robot_group = parser.add_argument_group("robot")
robot_group.add_argument("--robot", required=True, choices=("so101", "widowxai", "bimanual_widowxai"))
robot_group.add_argument("--port", help="Serial port (so101)")
robot_group.add_argument("--calibration", help="Calibration JSON path (so101)")
robot_group.add_argument("--ip", help="Robot IP (widowxai)")
robot_group.add_argument("--ip-left", help="Left arm IP (bimanual_widowxai)")
robot_group.add_argument("--ip-right", help="Right arm IP (bimanual_widowxai)")

# Model
model_group = parser.add_argument_group("model")
model_group.add_argument("--model", required=True, help="Exported model directory")
model_group.add_argument("--device", default="GPU", help="OpenVINO device (default: GPU)")

# Cameras
cam_group = parser.add_argument_group("cameras")
cam_group.add_argument(
"--camera", action="append", dest="cameras", metavar="NAME:DRIVER:DEVICE",
help="Camera as name:driver:device_id (repeatable). Omit for interactive selection.",
)
cam_group.add_argument("--cam-width", type=int, default=640, help="Camera width (default: 640)")
cam_group.add_argument("--cam-height", type=int, default=480, help="Camera height (default: 480)")
cam_group.add_argument("--cam-fps", type=int, default=30, help="Camera FPS (default: 30)")

# Runtime
rt_group = parser.add_argument_group("runtime")
rt_group.add_argument("--fps", type=float, default=30.0, help="Control loop FPS (default: 30)")
rt_group.add_argument("--duration-s", type=float, default=60.0, help="Duration in seconds")
rt_group.add_argument("--task", type=str, default=None, help="Task string for the model (e.g. 'pick up the can')")
rt_group.add_argument("--shared-camera", action="store_true", help="Use shared memory cameras (iceoryx2) — faster but incompatible with debugger")
rt_group.add_argument("--request-threshold", type=float, default=0.75, help="Request new inference when queue drops below this fraction of chunk_size (default: 0.75 = trigger when 75%% of actions remain)")
rt_group.add_argument("--lerp-frames", type=int, default=3, help="LerpSmoother blend duration in frames (default: 3)")

# Rerun
rr_group = parser.add_argument_group("rerun")
rr_group.add_argument("--rerun", choices=("off", "spawn", "connect", "save"), default="off")
rr_group.add_argument("--rerun-addr", default="127.0.0.1:9876")
rr_group.add_argument("--rerun-save-path", default="run.rrd")
rr_group.add_argument("--rerun-no-images", action="store_true", help="Scalars only")
rr_group.add_argument("--rerun-image-decimation", type=int, default=1, help="Only send 1/N frames to Rerun")
rr_group.add_argument("--rerun-jpeg-quality", type=int, default=None, help="JPEG quality for Rerun images (0-100, default: no re-encoding)")
rr_group.add_argument("--rerun-image-max-dim", type=int, default=None, help="Max width/height for Rerun images (default: no resizing)")

def main():
parser = argparse.ArgumentParser(description="Run policy with PolicyRuntime")
parser.add_argument("--model", required=True, help="Exported model directory")
parser.add_argument("--device", default="GPU.0", help="OpenVINO device")
parser.add_argument("--port", default="/dev/ttyACM0", help="Robot serial port")
parser.add_argument("--calibration", required=True, help="Robot calibration file")
parser.add_argument("--overhead-camera", required=True, help="Overhead camera device path")
parser.add_argument("--arm-camera", required=True, help="Arm camera serial number")
parser.add_argument("--front-camera", required=True, help="Front camera device path")
parser.add_argument("--width", type=int, default=640)
parser.add_argument("--height", type=int, default=480)
parser.add_argument("--duration-s", type=float, default=60.0)
parser.add_argument("--fps", type=float, default=30.0)
args = parser.parse_args()

# ── Load model ──
import openvino_tokenizers # noqa: F401 — registers OV tokenizer ops

print(f"Available devices:")
core = ov.Core()
devices = core.available_devices
for dev in devices:
print(f" {dev}: {core.get_property(dev, 'FULL_DEVICE_NAME')}")
print(f"Selected device: {args.device}")

print(f"Loading model from {args.model} on {args.device} (this may take a minute)...", flush=True)
model = InferenceModel.load(args.model, device=args.device)
robot = SO101(port=args.port, calibration=args.calibration, role="follower")
cameras = {
"overhead": SharedCamera("uvc", device=args.overhead_camera, width=args.width, height=args.height, fps=int(args.fps)),
"front": SharedCamera("uvc", device=args.front_camera, width=args.width, height=args.height, fps=int(args.fps)),
"arm": SharedCamera("realsense", serial_number=args.arm_camera, width=args.width, height=args.height, fps=int(args.fps)),
}

print("Model loaded.")

# ── Build robot & cameras ──
robot = build_robot(args)
if args.cameras:
cameras = parse_camera_specs(args.cameras, args.cam_width, args.cam_height, args.cam_fps, shared=args.shared_camera)
else:
cameras = select_cameras_interactive(args.cam_width, args.cam_height, args.cam_fps)

# ── Callbacks ──
callbacks: list = []
if args.rerun != "off":
callbacks.append(
RerunCallback(
cameras=cameras,
image_decimation=args.rerun_image_decimation,
log_images=not args.rerun_no_images,
image_jpeg_quality=args.rerun_jpeg_quality,
image_max_dim=args.rerun_image_max_dim,
mode=args.rerun,
connect_addr=args.rerun_addr,
save_path=args.rerun_save_path if args.rerun == "save" else None,
)
)

# ── Run ──
runtime = PolicyRuntime(
robot=robot,
model=model,
execution=AsyncExecution(threshold=0.3, fps=int(args.fps)),
action_queue=ActionQueue(smoother=LerpSmoother(duration_frames=5)),
execution=AsyncExecution(request_threshold=args.request_threshold, fps=int(args.fps)),
action_queue=ActionQueue(smoother=LerpSmoother(duration_frames=args.lerp_frames)),
cameras=cameras,
fps=args.fps,
callbacks=callbacks,
task=args.task,
)

try:
runtime.connect()
except Exception as e:
print(f"Failed to connect: {e}")
print("Available cameras:")
for driver, devices in discover_all().items():
for dev in devices:
print(f" Driver: {driver}, Device: {dev.device_id}, Info: {dev.name}")
return

for name, cam in cameras.items():
print(f"Camera '{name}' connected: {cam.actual_width}x{cam.actual_height} @ {cam.actual_fps}fps")

print("Starting policy runtime...")
try:
with runtime:
for name, cam in cameras.items():
w = getattr(cam, "actual_width", None)
h = getattr(cam, "actual_height", None)
f = getattr(cam, "actual_fps", None)
print(f" {name}: {w}x{h} @ {f}fps" if w and h else f" {name}: connected")
print(f"Running at {args.fps} fps for {args.duration_s}s...")
stats = runtime.run(duration_s=args.duration_s)
print(f"\nDone — {stats.steps} steps, {stats.inference_count} inferences, {stats.total_holds} holds")
finally:
runtime.disconnect()
print("Disconnected")

print(f"\nDone — {stats.steps} steps, {stats.inference_count} inferences, {stats.total_holds} holds")


if __name__ == "__main__":
Expand Down
Loading
Loading