Skip to content
Merged
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
104 changes: 104 additions & 0 deletions examples/runtime/async_inference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
# 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
"""

from __future__ import annotations

import argparse

import openvino as ov
import numpy as np

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


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()

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}")

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)),
}

runtime = PolicyRuntime(
robot=robot,
model=model,
execution=AsyncExecution(threshold=0.3, fps=int(args.fps)),
action_queue=ActionQueue(smoother=LerpSmoother(duration_frames=5)),
cameras=cameras,
fps=args.fps,
)

try:
runtime.connect()
except Exception as e:
print(f"Failed to connect: {e}")
print("Available cameras:")
for driver, devices in discover_all().items():
Comment thread
maxxgx marked this conversation as resolved.
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:
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")


if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion src/physicalai/inference/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Intel Corporation
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Production inference module for exported policies.
Expand Down
2 changes: 1 addition & 1 deletion src/physicalai/inference/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Intel Corporation
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Inference adapters for different backend runtimes.
Expand Down
2 changes: 1 addition & 1 deletion src/physicalai/inference/adapters/_discovery.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Intel Corporation
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Adapter discovery and factory helpers."""
Expand Down
2 changes: 1 addition & 1 deletion src/physicalai/inference/adapters/openvino.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Intel Corporation
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""OpenVINO adapter for inference."""
Expand Down
39 changes: 25 additions & 14 deletions src/physicalai/inference/model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2025-2026 Intel Corporation
# Copyright (C) 2026 Intel Corporation
# SPDX-License-Identifier: Apache-2.0

"""Production-ready inference model with unified API."""
Expand All @@ -7,21 +7,20 @@

import json
import warnings
from collections import deque
from pathlib import Path
from typing import TYPE_CHECKING, Any, Self

import numpy as np
import yaml

from physicalai.inference.adapters import adapter_registry, get_adapter
from physicalai.inference.component_factory import instantiate_component, resolve_artifact
from physicalai.inference.constants import ACTION
from physicalai.inference.manifest import ComponentSpec, Manifest
from physicalai.inference.runners import get_runner
from physicalai.inference.utils import ActionCursor

if TYPE_CHECKING:
import numpy as np

from physicalai.inference.adapters.base import RuntimeAdapter
from physicalai.inference.callbacks.base import Callback
from physicalai.inference.postprocessors.base import Postprocessor
Expand Down Expand Up @@ -125,7 +124,7 @@ def __init__(
for callback in self.callbacks:
callback.on_load(self)

self.cursor = ActionCursor()
self._action_buffer: deque[np.ndarray] = deque()

@property
def chunk_size(self) -> int:
Expand Down Expand Up @@ -207,31 +206,43 @@ def select_action(self, observation: dict[str, np.ndarray]) -> np.ndarray:
observation: Observation dict mapping names to numpy arrays.

Returns:
Action array to execute.
1-D action vector with shape ``(action_dim,)``.

Examples:
>>> obs = env.reset()
>>> action = policy.select_action(obs)
>>> next_obs, reward, done = env.step(action)
"""
if self.cursor.empty:
self.cursor.push_chunk(self(observation)[ACTION])

return self.cursor.pop()
if not self._action_buffer:
self._action_buffer.extend(self.predict_action_chunk(observation))
return self._action_buffer.popleft()

def predict_action_chunk(self, observation: dict[str, np.ndarray]) -> np.ndarray:
"""Predict a chunk of actions for the given observation.

Delegates to ``__call__`` and extracts the ``"action_chunk"`` key.
Delegates to ``__call__`` and extracts the ``"action"`` key.

Args:
observation: Observation dict mapping names to numpy arrays.

Returns:
Chunk of actions to execute.
2-D action chunk with shape ``(chunk_size, action_dim)``.

Raises:
ValueError: If the output has a batch dimension greater than 1.
"""
outputs = self(observation)
return outputs[ACTION]
actions = outputs[ACTION]
# Strip the batch dimension; reject actual batches (batch > 1).
if actions.ndim == 3: # noqa: PLR2004
if actions.shape[0] != 1:
msg = (
f"Batched inference is not supported by predict_action_chunk: "
f"expected batch dimension of 1, got shape {actions.shape}"
)
raise ValueError(msg)
actions = actions[0]
return np.atleast_2d(actions)

def reset(self) -> None:
"""Reset policy state for new episode.
Expand All @@ -250,7 +261,7 @@ def reset(self) -> None:
... obs, reward, done = env.step(action)
"""
self.runner.reset()
self.cursor.reset()
self._action_buffer.clear()
for callback in self.callbacks:
callback.on_reset()

Expand Down
8 changes: 5 additions & 3 deletions src/physicalai/inference/preprocessors/pi05.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,9 @@ def _preprocess_images(
if img.ndim == max_image_dim:
img = img[:, -1, :, :, :]

if img.dtype != np.float32:
if img.dtype == np.uint8:
img = img.astype(np.float32) / 255.0
elif img.dtype != np.float32:
img = img.astype(np.float32)
Comment thread
maxxgx marked this conversation as resolved.

# Detect layout: assume channels-first when dim-1 == 3
Expand All @@ -152,8 +154,8 @@ def _preprocess_images(
# [0, 1] -> [-1, 1]
img = img * 2.0 - 1.0

if channels_first:
img = np.transpose(img, (0, 3, 1, 2)) # -> (B, C, H, W)
# Output is always (B, C, H, W) — img is HWC at this point
img = np.transpose(img, (0, 3, 1, 2))

bsize = img.shape[0]
mask = np.ones(bsize, dtype=np.bool_)
Expand Down
8 changes: 0 additions & 8 deletions src/physicalai/inference/utils/__init__.py

This file was deleted.

90 changes: 0 additions & 90 deletions src/physicalai/inference/utils/action_cursor.py

This file was deleted.

10 changes: 9 additions & 1 deletion src/physicalai/robot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,20 @@

from __future__ import annotations

from typing import TYPE_CHECKING

from physicalai.robot.connect import connect
from physicalai.robot.interface import Robot
from physicalai.robot.interface import Robot, RobotObservation
from physicalai.robot.verify import verify_robot

if TYPE_CHECKING:
from physicalai.robot.so101 import SO101 as SO101
from physicalai.robot.trossen import BimanualWidowXAI as BimanualWidowXAI
from physicalai.robot.trossen import WidowXAI as WidowXAI

__all__ = [
"Robot",
"RobotObservation",
Comment thread
maxxgx marked this conversation as resolved.
"connect",
"verify_robot",
]
Expand Down
Loading
Loading