robosuite composite actions: check each robot part’s slice

If one flat action vector moves the wrong robot part, stop guessing its order from joint count or names in a policy config. Inspect the action map that the loaded robosuite composite controller exposes, build the vector from named part actions, and log that map with the policy. In an audited finite geometry example, changing only the assumed part order sent different joint and gripper values to each side; repacking from names restored the declared geometry within the 4 µm audit allowance.

Print the loaded map before inspecting action values

The robosuite composite-controller documentation describes one robot action as a set of body-part actions sent to their corresponding controllers. It recommends print_action_info() to see the split and create_action_vector() to assemble a correctly sized vector from named inputs. That is the right starting point because a plausible vector length does not identify which values belong to the right arm, left arm, base, torso or gripper.

Record the map beside every policy or dataset interface that emits the flat action. The useful record contains the total shape, each named half-open span such as start:end, the controller type for that part, and the loaded composite-controller mode. The pinned implementation’s get_action_info_dict() returns the total action shape and those named spans. A tensor shape by itself is incomplete provenance.

Diagnostic clue Check next Why the flat vector can still look valid
One arm reacts to the other arm’s command Printed part spans and loaded part order Both arm slices may have the same width
Gripper values appear shifted Gripper span and gripper DOF Gripper width is not inferred from the arm’s joint count
A custom controller changed the policy input width Controller type and control_dim in the loaded map Controller inputs need not equal mechanical joint DOF
Whole-body mode parses an old BASIC vector incorrectly Public WholeBody action map Its policy-facing input can differ from the underlying part-controller layout

BASIC slices follow configured parts and controller inputs

In the pinned 1.5.2 source, the BASIC composite controller optionally rebuilds its ordered part configuration from body_part_ordering. It then visits the loaded part controllers in that order. The split construction and goal-routing code advances a running index by gripper.dof for a gripper and by controller.control_dim for another part. set_goal() slices the flat vector with those stored bounds.

This is why “left arm first” and “one number per joint” are unsafe assumptions. Part order is configuration-dependent, and controller input dimensions can change with the controller interface. A variable controller may add input terms even though the robot’s mechanical joints did not change. The gripper path also calls format_action() after extracting its slice and before forwarding the result to the part controller. Logging only the pre-format flat vector can therefore miss the representation that the gripper controller actually receives.

The current documentation page displays version 1.5, while the pinned release used here is robosuite v1.5.2. Treat the code pin as the claim boundary rather than assuming a living documentation label promises identical behavior in every later release.

Named assembly prevents order guesses, with limits

The pinned create_action_vector() implementation begins with a zero-filled vector, looks up each supplied part name, checks the supplied slice length for a known nonempty part, and writes it into that part’s span. This makes a dictionary of named part actions safer than manual concatenation.

It is not complete semantic validation. In this code path:

  1. A supplied name absent from the action map is logged at debug level and skipped.
  2. A known part omitted from the dictionary keeps the zero values created at initialization.
  3. The assertion checks the length of a supplied known slice, not whether its values use the intended units, frame, limits or controller meaning.
  4. Zero is not universally neutral, safe or a no-op; its effect depends on the receiving controller and configuration.

After named assembly, convert the vector back to a named dictionary or print the map and compare every slice. That round trip catches misspelled part names and accidental omissions that a total-length check can miss. Keep the gripper’s post-format behavior in the trace when a gripper is involved.

WholeBody has a separate policy-facing layout

WholeBody is not merely BASIC with a longer vector. The pinned source states that it accepts an input space different from the underlying factorized part controllers. Its split setup and goal path first builds the underlying actuation-part spans, then maintains a separate WholeBody input map. The WholeBody information and vector helpers use that public map unless skip_wbc_action delegates to the BASIC behavior.

For a migration, log both the composite-controller name and the map returned by that loaded mode. Reusing an old concatenation because the total length happens to match can silently reassign values to different meanings. The map is the contract; the policy’s remembered order is only an assumption until it matches.

The same vector produced different named targets

The audited Blender 5.2.2 geometry used 51 closed boxes for two two-link arms, palms, fingers, endpoint markers and one fixture. There was no base object. The two links on each arm measured 0.35 m and 0.25 m. Three cases were arranged as separate static prescribed poses; they were not frames from an animation or measurements from one initial pose.

The declared BASIC-compatible order was right, right_gripper, left, left_gripper. It received the flat vector [0.4, -0.75, 0.3, -0.35, 0.7, -0.6]. The wrong-order case interpreted that same vector as left, left_gripper, right, right_gripper, so the right arm received [-0.35, 0.7] instead of [0.4, -0.75], while the right gripper received -0.6 instead of 0.3.

Case Flat-vector handling Measured right / left aperture Right endpoint-marker relation to fixture
Declared BASIC Original vector under right-first metadata 46 mm / 28 mm Separating projection gap 55.572 mm
Assumed left-first Same vector under left-first metadata 28 mm / 46 mm Fully contained; conservative face inset 19.290 mm
Named repack Values reordered for left-first metadata 46 mm / 28 mm Separating projection gap 55.572 mm

The repacked vector was [-0.35, 0.7, -0.6, 0.4, -0.75, 0.3]. It restored every named input from the declared case. The independent audit found every resulting finite vertex within the frozen 4 µm world-vertex allowance; that is a tolerance-bounded geometric match, not a claim of byte-identical scene files.

The arm equations and fixture bounds were defined in a common coordinate frame. Blender stored the evaluated objects in world coordinates after one proper Euler XYZ rotation and translation. The audit recovered the common-frame endpoint centers from all eight corners of each finite endpoint marker, then checked the marker corners against every fixture face. Treating a world-coordinate endpoint as if it were already in the common frame would mix two different representations.

Finite dual-arm geometry comparing declared, wrongly assumed and named-repacked composite action layouts.
The same flat values reach different named parts under the wrong order; named repacking restores the declared inputs. Static prescribed Blender geometry, not a robosuite controller or dynamics run.

The downstream mappings were authored for this illustration: each arm channel set one relative planar angle at 1 radian per action unit, and gripper input g set aperture to 0.04 + 0.02g metres. They are not robosuite controller laws, JOINT_POSITION behavior or executed format_action() semantics.

The fixture result is equally narrow. It classifies only the finite right endpoint marker as fully contained or separated by a fixture-face projection. The 55.572 mm value is not a minimum whole-arm clearance. Nothing here establishes whole-arm collision, path clearance, contact behavior, controller tracking, IK, dynamics, MuJoCo behavior, hardware performance or safety.

Trace routing before tuning the controller

Use this order when a composite action produces the wrong motion:

  1. Record the loaded composite-controller name and release pin.
  2. Print the total action shape and every named start:end span.
  3. Build one test action from named part inputs instead of manual concatenation.
  4. Convert it back to named slices and compare each supplied part, including omissions.
  5. Trace gripper formatting and then the exact part controller that receives each slice.
  6. Only after routing matches should you inspect that controller’s target semantics, scaling or gains.

If the slice reaches OSC but the target still behaves unexpectedly, the separate OSC delta-reference check asks whether the increment is based on measured position or the previous goal. If the command is correct and the remaining mismatch is between actuator output and generalized force, continue with the actuator-to-joint torque mapping. Those are downstream questions. First prove that each action component reached the intended robot part.

Reproduce the scalar routing check

This standalone script reproduces the named slicing, repack, authored planar endpoint equations and endpoint-marker relation in the declared common frame. It does not import or run robosuite or Blender.

"""Illustrative scalar routing/finite geometry only; no robosuite or Blender run."""
from math import cos, sin
from itertools import product

widths = {'right': 2, 'right_gripper': 1, 'left': 2, 'left_gripper': 1}
basic = ['right', 'right_gripper', 'left', 'left_gripper']
left_first = ['left', 'left_gripper', 'right', 'right_gripper']
action = [0.4, -0.75, 0.3, -0.35, 0.7, -0.6]
fixture = [(0.52, 0.60), (-0.42, -0.36), (-0.045, 0.045)]


def split(order, vector):
    if len(order) != len(widths) or set(order) != set(widths):
        raise ValueError('Every named part must occur once')
    if len(vector) != sum(widths.values()):
        raise ValueError('Wrong vector length')
    cursor, result = 0, {}
    for name in order:
        result[name] = vector[cursor:cursor + widths[name]]
        cursor += widths[name]
    return result


def endpoint(named, arm):
    # These are our declared rad/unit geometry rules, not a controller law.
    q1, q2 = named[arm]
    shoulder_y = -0.35 if arm == 'right' else 0.35
    return (0.35*cos(q1) + 0.25*cos(q1+q2),
            shoulder_y + 0.35*sin(q1) + 0.25*sin(q1+q2), 0.0)


def marker_relation(point):
    # Complete corners of a 10 mm cube, axes in the declared common frame.
    corners = [tuple(point[j] + 0.005*sign[j] for j in range(3))
               for sign in product((-1, 1), repeat=3)]
    inside = all(lo < corner[j] < hi
                 for corner in corners for j, (lo, hi) in enumerate(fixture))
    if inside:
        return 'endpoint marker strictly contained'
    for j, (lo, hi) in enumerate(fixture):
        if max(c[j] for c in corners) < lo or min(c[j] for c in corners) > hi:
            return 'endpoint marker separated by a fixture plane'
    return 'boundary/crossing case; this short check makes no further claim'


named = split(basic, action)
repacked = [value for name in left_first for value in named[name]]
cases = [('declared BASIC', basic, action),
         ('assumed left-first', left_first, action),
         ('named repack', left_first, repacked)]
for label, order, vector in cases:
    received = split(order, vector)
    print(label, 'vector=', vector)
    for arm in ('right', 'left'):
        tip = endpoint(received, arm)
        aperture = 0.04 + 0.02*received[arm + '_gripper'][0]
        print(' ', arm, 'joint rad=', received[arm],
              'endpoint m=', tuple(round(x, 6) for x in tip),
              'aperture m=', round(aperture, 6), marker_relation(tip))
assert split(left_first, repacked) == named

The script checks scalar routing and the authored common-frame marker relation. The native evidence additionally measured all evaluated finite vertices after the proper common-to-world transform. Neither result executes a robosuite controller.