Repository navigation
Benchmark seven more atomic skills - #642
Yuan-Xinyi wants to merge 10 commits into
Conversation
|
| "open_door": "scripts.benchmark.atomic_action.open_door_benchmark", | ||
| "press": "scripts.benchmark.atomic_action.press_benchmark", | ||
| "slide": "scripts.benchmark.atomic_action.slide_benchmark", | ||
| "twist": "scripts.benchmark.atomic_action.twist_benchmark", | ||
| "axis_align": "scripts.benchmark.atomic_action.axis_align_benchmark", | ||
| "pour": "scripts.benchmark.atomic_action.pour_benchmark", | ||
| "hand_over": "scripts.benchmark.atomic_action.hand_over_benchmark", |
There was a problem hiding this comment.
In-process actions lack required arguments
The new actions cannot run through --in_process: _make_child_args() omits their case-selection fields, but each new run_all_benchmarks() accesses its field directly before applying the smoke profile. For example, --action open_door --in_process --profile smoke raises AttributeError on args.door_cases instead of producing a report. Populate the new action-specific defaults when constructing the child namespace, preferably using each module's argument parser.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/run_benchmark.py
Line: 46-52
Comment:
**In-process actions lack required arguments**
The new actions cannot run through `--in_process`: `_make_child_args()` omits their case-selection fields, but each new `run_all_benchmarks()` accesses its field directly before applying the smoke profile. For example, `--action open_door --in_process --profile smoke` raises `AttributeError` on `args.door_cases` instead of producing a report. Populate the new action-specific defaults when constructing the child namespace, preferably using each module's argument parser.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| unit_axis = axis / torch.linalg.vector_norm(axis) | ||
| sign = torch.sign(torch.dot(vector, unit_axis.to(vector.dtype))) | ||
| if float(sign) == 0.0: | ||
| sign = torch.ones_like(sign) | ||
| return float(angle * sign) |
There was a problem hiding this comment.
Off-axis tumbles count as pours
This helper returns the total relative rotation angle; the requested axis only determines its sign. A 45-degree tumble perpendicular to the pour axis therefore returns positive 45 degrees because the zero dot product is forced to a positive sign. The 45-degree benchmark can consequently credit an off-axis slip as a successful pour. Measure rotation about the requested axis and add a perpendicular-axis counterexample test.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/pour_benchmark.py
Line: 146-150
Comment:
**Off-axis tumbles count as pours**
This helper returns the total relative rotation angle; the requested axis only determines its sign. A 45-degree tumble perpendicular to the pour axis therefore returns positive 45 degrees because the zero dot product is forced to a positive sign. The 45-degree benchmark can consequently credit an off-axis slip as a successful pour. Measure rotation about the requested axis and add a perpendicular-axis counterexample test.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| rotation_error = abs( | ||
| abs(trace.peak_signed_displacement) - abs(case.rotate_angle_rad) | ||
| ) |
There was a problem hiding this comment.
Taking the absolute value of both rotations removes the direction check from the signed task metric. If a slipping object rotates −45 degrees around the requested axis for the +45-degree case, this reports zero error and task_success=True. The measurement and primitive use the same axis convention, so compare the signed achieved rotation with the signed command rather than their magnitudes.
| rotation_error = abs( | |
| abs(trace.peak_signed_displacement) - abs(case.rotate_angle_rad) | |
| ) | |
| rotation_error = abs( | |
| trace.peak_signed_displacement - case.rotate_angle_rad | |
| ) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/pour_benchmark.py
Line: 294-296
Comment:
**Reversed pours pass the gate**
Taking the absolute value of both rotations removes the direction check from the signed task metric. If a slipping object rotates −45 degrees around the requested axis for the +45-degree case, this reports zero error and `task_success=True`. The measurement and primitive use the same axis convention, so compare the signed achieved rotation with the signed command rather than their magnitudes.
```suggestion
rotation_error = abs(
trace.peak_signed_displacement - case.rotate_angle_rad
)
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| min_z = state["min_z"] if state["min_z"] != float("inf") else None | ||
| dropped = min_z is not None and min_z < HANDOVER_DROP_Z_M | ||
| if trace is not None and ( | ||
| trace.max_tracking_error_rad > REPLAY_TRACKING_TOLERANCE_RAD | ||
| ): | ||
| ladder.execution_success = False | ||
| ladder.fail("execution_success", "controller_tracking_failure") | ||
| elif ladder.execution_success and trace is not None: | ||
| delivered = trace.settled_position <= HANDOVER_DELIVERY_TOLERANCE_M | ||
| ladder.task_success = delivered and not dropped |
There was a problem hiding this comment.
Tabletop drops escape detection
The drop threshold is below the support table, so it cannot detect an object dropped onto that table during transfer. The target lies inside the table footprint, only 0.10 m above its surface. An object resting near the target at z≈0.53 m passes both the 0.12 m delivery tolerance and the z≥0.35 m check despite losing its grasp prematurely. Track grasp retention or an unexpected return to the support surface during transfer, while allowing the primitive's intentional final release.
Knowledge Base Used: Simulation lab
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/hand_over_benchmark.py
Line: 238-247
Comment:
**Tabletop drops escape detection**
The drop threshold is below the support table, so it cannot detect an object dropped onto that table during transfer. The target lies inside the table footprint, only 0.10 m above its surface. An object resting near the target at z≈0.53 m passes both the 0.12 m delivery tolerance and the z≥0.35 m check despite losing its grasp prematurely. Track grasp retention or an unexpected return to the support surface during transfer, while allowing the primitive's intentional final release.
**Knowledge Base Used:** [Simulation lab](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/simulation-lab.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| recorded = replay_trajectory_with_recording( | ||
| sim=sim, | ||
| robot=robot, | ||
| traj=traj, | ||
| args=args, | ||
| video_path=build_video_output_path( |
There was a problem hiding this comment.
Videos replay a different execution
The recorder uses four physics steps per waypoint, whereas OpenDoor is scored at 64 and the other new benchmarks at 16. For these contact tasks, that changes tracking and achieved motion—not just playback speed—so a video attached to a successful case can depict a different outcome. Record during the measured replay or give the recording path the same replay cadence and terminal hold settings.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/open_door_benchmark.py
Line: 286-291
Comment:
**Videos replay a different execution**
The recorder uses four physics steps per waypoint, whereas OpenDoor is scored at 64 and the other new benchmarks at 16. For these contact tasks, that changes tracking and achieved motion—not just playback speed—so a video attached to a successful case can depict a different outcome. Record during the measured replay or give the recording path the same replay cadence and terminal hold settings.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| video_path = str(recorded) if recorded is not None else "" | ||
| video_paths.append(video_path) |
There was a problem hiding this comment.
Failed recordings consume video quota
A failed or unavailable recording is appended to video_paths as an empty string. With --record_video --record_failed_video and the default case limit, a planning failure with no trajectory can consume the quota, suppress a later recordable case, and inflate the reported video count. Append only nonempty recording paths, as the other new benchmarks do. Twist has the same issue.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/slide_benchmark.py
Line: 318-319
Comment:
**Failed recordings consume video quota**
A failed or unavailable recording is appended to `video_paths` as an empty string. With `--record_video --record_failed_video` and the default case limit, a planning failure with no trajectory can consume the quota, suppress a later recordable case, and inflate the reported video count. Append only nonempty recording paths, as the other new benchmarks do. Twist has the same issue.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| state["pour_start_z"], | ||
| float(obj.get_local_pose(to_matrix=True)[0, 2, 3]), |
There was a problem hiding this comment.
Recording changes the height diagnostic
final_object_z_m is read after the optional reset and recording replay, while the rotation trace and starting height describe the scored run. Enabling recording can therefore replace this diagnostic with the height from a different execution—or the reset pose if recording cannot start. Capture the final height immediately after physical validation, before resetting the scene for video.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/pour_benchmark.py
Line: 329-330
Comment:
**Recording changes the height diagnostic**
`final_object_z_m` is read after the optional reset and recording replay, while the rotation trace and starting height describe the scored run. Enabling recording can therefore replace this diagnostic with the height from a different execution—or the reset pose if recording cannot start. Capture the final height immediately after physical validation, before resetting the scene for video.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| return [AXIS_ALIGN_CASES[name] for name in case_names] | ||
|
|
||
|
|
||
| def object_axis_angle_rad(obj, internal_axis, target_axis) -> float: |
There was a problem hiding this comment.
Exported helpers lack parameter annotations
The exported object_axis_angle_rad() leaves all three parameters unannotated, violating the repository directive to fully annotate public APIs. Add RigidObject and tensor parameter annotations, using type-only imports where appropriate. The exported signed_rotation_about_axis_rad() in Pour has the same omission. This repository requirement must be satisfied before merging.
Context Used: CLAUDE.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/axis_align_benchmark.py
Line: 115
Comment:
**Exported helpers lack parameter annotations**
The exported `object_axis_angle_rad()` leaves all three parameters unannotated, violating the repository directive to fully annotate public APIs. Add `RigidObject` and tensor parameter annotations, using type-only imports where appropriate. The exported `signed_rotation_about_axis_rad()` in Pour has the same omission. This repository requirement must be satisfied before merging.
**Context Used:** CLAUDE.md ([source](https://github.com/dexforce/embodichain/blob/main/CLAUDE.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| actually got there. Sweeping the rate on OpenDoor: | ||
|
|
||
| | Steps/waypoint | Max arm tracking error (rad) | Hinge error (rad) | | ||
| |---|---|---| | ||
| | 1 (planner's own dt) | 1.3844 | 0.2273 | | ||
| | 4 (original) | 0.4390 | 0.0689 | | ||
| | 16 (**chosen**) | 0.0532 | 0.0306 | | ||
| | 32 | 0.0230 | 0.0245 | | ||
|
|
||
| The measured "task" error was largely the controller failing to follow the plan. | ||
| I first tried replaying at the planner's own `dt`, which is *worse* — it made | ||
| OpenDoor and Slide fail outright. | ||
|
|
||
| **Fix**: `DEFAULT_REPLAY_STEPS_PER_WAYPOINT = 16`, plus a new |
There was a problem hiding this comment.
Report describes outdated replay settings
The report presents 16 steps per waypoint as OpenDoor's chosen replay rate, but the committed benchmark uses 64 because 16 fails its tracking gate. Together with the opening claim that the changes are uncommitted, this leaves readers unable to tie the reported results to the reviewed implementation. Identify the measured revision and settings, and separate historical experiments from the current benchmark configuration.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/BENCHMARK_REPORT.md
Line: 73-86
Comment:
**Report describes outdated replay settings**
The report presents 16 steps per waypoint as OpenDoor's chosen replay rate, but the committed benchmark uses 64 because 16 fails its tracking gate. Together with the opening claim that the changes are uncommitted, this leaves readers unable to tie the reported results to the reviewed implementation. Identify the measured revision and settings, and separate historical experiments from the current benchmark configuration.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.13cf9bb to
b0eece2
Compare
67a817a to
2d7adeb
Compare
Rebased onto the measurement standard. Adds open_door, press, slide, twist, axis_align, pour and hand_over benchmarks, the shared contact-skill measurement layer they run on, and their entries in the suite dispatcher. Dropped from the earlier revision of this branch: - Four commits whose content reached main through #620. Their five benchmark files are now byte-identical to main, and replaying them would have removed the affordance_sampling support that landed after the branch point. - Its own copy of BENCHMARK_STANDARD.md, superseded by the one on the base branch. - The press and slide tutorial strategy fix, which is already on main; both tutorials request strategy="ik_interp" there today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The suite shipped its own four-level ladder (planning_success, motion_valid, execution_success, task_success), legacy tolerances, and a tracking gate. The standard on the base branch replaced all three, so the benchmarks measured something the document no longer describes. Measurement layer: - StageLadder replaces SuccessLadder. Stages come from SKILL_STAGES, which mirrors section 1 for all fifteen skills; a stage is reached only when every earlier one passed, and recording past the first failure is ignored. - build_stage_leaderboard reports passed(i)/reached(i) per stage plus the overall success rate, with N/A for a stage no case reached. - Plan validity and replay tracking are diagnostics: the tracking gate and its REPLAY_TRACKING_TOLERANCE_RAD are gone, and the taxonomy is the nine task-level reasons, not seventeen. - PRIMITIVE_*/TASK_* tolerances replace PHYSICAL_PLACE_XY_TOLERANCE_M (0.10) and PHYSICAL_MOVE_HELD_OBJECT_XYZ_TOLERANCE_M (0.12). - dropped_below_support applies the cross-cutting drop rule against the lowest point of a replay; hand_is_released measures a release against the skill's own open and closed commands rather than a per-asset threshold. - replay_and_track_channels samples several scalars in one replay, so a skill scored on one signal can report another. replay_and_track_scalar is now a wrapper over it, and the physical-validation replay runs at the calibrated 16 steps per waypoint instead of 4. - A missing actuation segment fails the case as invalid_case instead of silently scoring the trajectory's last waypoint, which is the rebound the helper exists to avoid. Per skill: - MoveJoints and MoveEndEffector are measured on the robot after a physical replay and a terminal settle, not from traj[:, -1], which reported 0.000000 rad and 0.00002 m and described the solver. - PickUp, Place, AxisAlign, MoveHeldObject, OpenDoor, Slide, Press, Pour and HandOver report their stages from the standard's table, including release and settle stages that were not measured before. - Twist scores the executed end-effector rotation about the knob axis, with the axis probed from the asset; the knob joint, which over-rotates because the gripper wedges against it, is now a diagnostic. - HandOver's delivery budget is the derived 3 x TASK_POSITION_TOLERANCE_M. Tests read BENCHMARK_STANDARD.md and check the stage table, the failure taxonomy and the tolerance constants against it, so the code cannot drift from the document, plus the stage bookkeeping and the two shared scene rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Replaces the earlier report, which described the four-level ladder and carried smoke-profile results for the five pre-existing benchmarks. Every skill is now a coverage run under the standard, presented as the stage chain that produces its success_rate. Eight skills pass every case. Four do not, and the report says where each loses them: - pick_up 52.08%: 15 of 48 cases report a held object while the object never leaves the table, which only the separate grasped and lifted stages expose. - place 37.50%: the tighter tolerance accounts for 7 cases; 17 more miss by over 10 cm. - move_held_object 27.08%: median position error 0.5411 m, so not a tolerance effect; at the previous 0.12 m only 15 of 40 would pass. - hand_over 0%: both cases fail only at placed, against the derived 3 cm budget. Also records the control run showing the 4 to 16 steps-per-waypoint replay change leaves pick_up's rates identical, and keeps the two criteria that do not discriminate: press at any commanded depth, and Twist's knob joint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2d7adeb to
3df6531
Compare
--in_process exposes all twelve actions, but _make_child_args() built the child
namespace from a hand-kept list of the five original benchmarks' arguments. The
seven added here read their own case selection -- door_cases, press_cases,
slide_cases, twist_cases, align_cases, pour_cases, hand_over_cases -- and every
one raised AttributeError on its first line of work, the smoke profile
included:
python -m scripts.benchmark.atomic_action.run_benchmark \
--action press --profile smoke --device cpu --in_process
AttributeError: 'Namespace' object has no attribute 'press_cases'
The namespace now starts from the selected module's own parser defaults, so a
benchmark's arguments are always the ones it declares, and the aggregate run's
shared settings are applied over them. Object, position and approach
selections are forwarded only when the aggregate run was given them, and a
shared setting is applied only where the module declares it, so a skill that
samples no grasps is not handed n_sample.
Tests parametrize over every registered action, compare the child namespace
against the attributes each benchmark module actually reads, and check that
each action keeps its own case-selection defaults. Against the namespace this
commit replaces, those tests fail for all seven added actions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| ladder.record( | ||
| "transferred", | ||
| traces["hand_balance"].min_position < 0.0 and not dropped, | ||
| "object_dropped" if dropped else "task_goal_miss", |
There was a problem hiding this comment.
hand_balance is distance-to-left minus distance-to-right, so a negative value means the object is closer to the left hand—the opposite of the comment's convention for the configured left-to-right transfer. Its minimum also includes the initial pickup, not just the transfer boundary. A successful source grasp followed by a missed receiving grasp can therefore report transferred=True, incorrectly blaming a later stage instead.
Measure both hands holding at the transfer boundary, using the primitive's actual selected arm assignment rather than a whole-replay distance minimum.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/hand_over_benchmark.py
Line: 292-295
Comment:
**Transfer passes during pickup**
`hand_balance` is distance-to-left minus distance-to-right, so a negative value means the object is closer to the left hand—the opposite of the comment's convention for the configured left-to-right transfer. Its minimum also includes the initial pickup, not just the transfer boundary. A successful source grasp followed by a missed receiving grasp can therefore report `transferred=True`, incorrectly blaming a later stage instead.
Measure both hands holding at the transfer boundary, using the primitive's actual selected arm assignment rather than a whole-replay distance minimum.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| # The last waypoint is where Place stops acting on the object; | ||
| # everything after it is the terminal settle. | ||
| nonlocal segment_end_pose | ||
| segment_end_pose = obj.get_local_pose(to_matrix=True)[0].clone() |
There was a problem hiding this comment.
Stability excludes withdrawal motion
segment_end_pose is overwritten at every waypoint, so the stability reference is taken after the complete trajectory. Place has a separate retract segment after release: an object can topple during withdrawal and stop before the final waypoint, producing zero measured settle drift. If its XY position remains within tolerance, all stages pass despite the topple.
Capture the pose at the release boundary and compare it with the settled pose so the stability check includes movement during withdrawal.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/place_benchmark.py
Line: 384-387
Comment:
**Stability excludes withdrawal motion**
`segment_end_pose` is overwritten at every waypoint, so the stability reference is taken after the complete trajectory. Place has a separate `retract` segment after `release`: an object can topple during withdrawal and stop before the final waypoint, producing zero measured settle drift. If its XY position remains within tolerance, all stages pass despite the topple.
Capture the pose at the release boundary and compare it with the settled pose so the stability check includes movement during withdrawal.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| # The skill's own held-object state is the grasp measurement: it is | ||
| # what every later stage is asserted against. | ||
| ladder.record("grasped", held_created, "object_not_grasped") |
There was a problem hiding this comment.
Planned effects count as observations
held_created comes from result.projected_context, which creates a hypothetical held relation during compilation without checking physical attachment. A missed physical grasp therefore counts as grasped=True. Place's new released stage similarly uses the planner's projected removal of that relation, so an object still physically held can count as released.
These values corrupt the physical stage rates and failure localization promised by the measurement standard. Keep projected state as a diagnostic, and determine grasp and release from replay observations at their respective boundaries.
Knowledge Base Used: Simulation lab
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/pickup_benchmark.py
Line: 268-270
Comment:
**Planned effects count as observations**
`held_created` comes from `result.projected_context`, which creates a hypothetical held relation during compilation without checking physical attachment. A missed physical grasp therefore counts as `grasped=True`. Place's new `released` stage similarly uses the planner's projected removal of that relation, so an object still physically held can count as released.
These values corrupt the physical stage rates and failure localization promised by the measurement standard. Keep projected state as a diagnostic, and determine grasp and release from replay observations at their respective boundaries.
**Knowledge Base Used:** [Simulation lab](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/simulation-lab.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| ladder.record( | ||
| "opened", hinge_error <= TASK_ROTATION_TOLERANCE_RAD, "task_goal_miss" | ||
| ) | ||
| hand_open, hand_close = get_hand_open_close_qpos(robot) |
There was a problem hiding this comment.
Release uses mismatched closed command
The engine uses close_qpos=0.04, but this call uses the helper's default 0.036. With an open command of zero, an achieved jaw position of 0.019 is closer to the actual open command than to 0.04, yet this check reports release failure because it compares against 0.036. This can misclassify borderline releases. Use the same explicit close value here, or pass the original command tensors into the evaluator.
| hand_open, hand_close = get_hand_open_close_qpos(robot) | |
| hand_open, hand_close = get_hand_open_close_qpos(robot, close_qpos=0.04) |
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/open_door_benchmark.py
Line: 288
Comment:
**Release uses mismatched closed command**
The engine uses `close_qpos=0.04`, but this call uses the helper's default `0.036`. With an open command of zero, an achieved jaw position of `0.019` is closer to the actual open command than to `0.04`, yet this check reports release failure because it compares against `0.036`. This can misclassify borderline releases. Use the same explicit close value here, or pass the original command tensors into the evaluator.
```suggestion
hand_open, hand_close = get_hand_open_close_qpos(robot, close_qpos=0.04)
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.With the child arguments fixed, --action all --in_process reaches hand_over and
fails there:
ValueError: Robot DualUr5HandOver not found
base_planner.py:220, SimulationManager.get_instance(...).get_robot(...)
The simulator is a singleton keyed by instance id and a planner resolves its
robot through that instance. Each benchmark builds its own scene, so leaving
the previous one standing makes the next benchmark a second instance whose
planner then looks for its robot among the first benchmark's robots. The
subprocess path never sees this because every action gets a fresh process.
HandOver already tore its scene down between its own cases. That helper moves
to common as release_simulation(), and the dispatcher calls it after each
in-process benchmark, so a benchmark always starts from a released singleton.
Verified with press followed by hand_over in one process: both run and both
write their report.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The report says where each of the four skills loses its cases; these are the cases themselves, captured with the suite's own --record_video --record_failed_video --video_case_limit 1 and converted to gif at the size docs/source/_static already uses. - pick_up sugar_box:q1_near:side: the hand closes, the arm follows its plan, the object never leaves the table (lift -0.000147 m). - place sugar_box:q1_near:top:left_bin: the hand opens 0.4286 m from the commanded pose. - move_held_object sugar_box:q1_near:top:front_left: 0.8288 m and 1.5911 rad off, the worst of the four. - hand_over vertical_can: a clean transfer that misses only the delivery, at 0.1981 m. A clip's numbers come from the run that produced it rather than from the coverage run, which the report states: grasp sampling is stochastic and every run is --repeat 1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
pick_up scores 52.08 % over coverage, place 37.50 % and move_held_object 27.08 %, and measuring where those cases go shows most of them are not skill failures at all: - Objects 0.18 m from the base sit in the arm's inner workspace. Every IK branch for such a grasp folds the shoulder back on itself, and driving the arm to any of them stalls: j1 stops 0.44 rad short with every other joint tracking to 0.01 rad and the limits nowhere near. - A side approach asks for a horizontal gripper at the height of an object lying on the ground. Probing the same case at increasing heights, 0 of the sampled grasps are attainable at the object's own height and 3 of 3 become attainable once lifted 0.20 m, while the top approach is attainable at every height. The gripper is hitting the ground. Both produce a plan that executes into thin air, and the skill's feasibility screen cannot see either: it accepts a candidate when IK returns a solution, which says a configuration exists, not that the arm can get to it. has_attainable_grasp_candidate() drives each sampled grasp in physics, pre-grasp then grasp, and stops at the first one that arrives. When none does, the case is recorded with unsupported_capability, leaves every denominator and lowers coverage_rate instead, which is what BENCHMARK_DESIGN.md section 5 prescribes for an unsupported case and what the standard now states for success_rate. Supported cases pay one probe; only an unsupported one pays for the whole candidate list. The cases themselves are not touched: the positions, approaches and object presets are #318's, unchanged here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three defects in the probe as first wired up, all found by running it: - move_held_object reset the robot through a helper its scope never imported, so eleven cases raised NameError and were recorded as planner exceptions instead of unsupported. - The probe drives the arm into grasp poses, which can nudge the object it is probing. It now restores the object pose as well as the robot's. - move_held_object asked the question from the home pose while pick_up and place asked it from the pre-pick pose, and a seeded IK gives different branches from different seeds. It now probes from the pre-pick pose like the others and puts the robot back afterwards, so the case itself still runs from the state it always did. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every skill re-measured at --profile coverage with the attainability probe in place. Eight skills pass every case they were given; five lose cases, and the report says where each one loses them: - pick_up 74.19 % of 31 measured, all eight losses at lifted. - place 48.28 % of 29 measured, fifteen at placed, median XY error 0.0101 m against the 1 cm tolerance with a tail to 0.6373 m. - move_held_object 48.15 % of 27 measured, median 0.0064 m with a tail to 0.6478 m. - hand_over 0 % of 2, both failing only at placed. - open_door 66.67 % of 3, open_90 never moving the hinge. About 40 % of the three grasp suites' cases are now unsupported_capability rather than failures: 9 to 11 of the 12 q1 cases, which sit inside the arm's inner workspace, and 13 to 16 of the 24 side approaches, which ask for a horizontal gripper at the height of an object lying on the ground. Section 3 carries the measurements behind both. Excluding them raised pick_up from 52.08 % to 74.19 %, place from 37.50 % to 48.28 % and move_held_object from 27.08 % to 48.15 %, and took grasped and released to 100 %. Section 4 is new and says plainly that these are single-sample estimates: open_door scored 100 % and then 66.67 % on two runs of the same code, which is the honest caveat on every rate here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| for index in range(poses.shape[0]): | ||
| if tested >= GRASP_ATTAINABILITY_MAX_CANDIDATES: | ||
| break | ||
| if not bool(valid[index]): | ||
| continue | ||
| grasp_pose = poses[index : index + 1] |
There was a problem hiding this comment.
Incomplete probes exclude supported cases
The probe stops after eight IK-feasible candidates, but the generator can return 50 and PickUp considers the full set, including mirrored TCP-roll variants that this probe never checks. When the tested poses stall but a later candidate or mirrored variant is attainable, this returns false before the skill runs. PickUp, Place and MoveHeldObject then mark a supported case as unsupported and remove it from their success denominators.
Check the skill's supported candidate variants exhaustively before declaring unsupported, or distinguish an exhausted probe budget from demonstrated lack of capability.
Knowledge Base Used: GraspKit workflows
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/common.py
Line: 1415-1420
Comment:
**Incomplete probes exclude supported cases**
The probe stops after eight IK-feasible candidates, but the generator can return 50 and PickUp considers the full set, including mirrored TCP-roll variants that this probe never checks. When the tested poses stall but a later candidate or mirrored variant is attainable, this returns false before the skill runs. PickUp, Place and MoveHeldObject then mark a supported case as unsupported and remove it from their success denominators.
Check the skill's supported candidate variants exhaustively before declaring unsupported, or distinguish an exhausted probe budget from demonstrated lack of capability.
**Knowledge Base Used:** [GraspKit workflows](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/graspkit.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| robot.set_qpos(initial_qpos, target=False) | ||
| robot.set_qpos(initial_qpos, target=True) | ||
| robot.clear_dynamics() | ||
| sim.update(step=GRASP_ATTAINABILITY_HOLD_STEPS) |
There was a problem hiding this comment.
Probe attempts share displaced objects
Each attempt restores the robot but leaves the dynamic object wherever the previous attempt pushed it. All candidates were sampled against the original object pose, so later attempts can run against a displaced target. An arm configuration can then appear attainable because an earlier attempt pushed the obstruction away, or become blocked by the displaced object. The resulting unsupported classification depends on probe order rather than the original benchmark scene.
Restore the object's pose and clear its dynamics before every candidate attempt, not only when the helper returns.
Knowledge Base Used: Simulation lab
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/common.py
Line: 1434-1437
Comment:
**Probe attempts share displaced objects**
Each attempt restores the robot but leaves the dynamic object wherever the previous attempt pushed it. All candidates were sampled against the original object pose, so later attempts can run against a displaced target. An arm configuration can then appear attainable because an earlier attempt pushed the obstruction away, or become blocked by the displaced object. The resulting unsupported classification depends on probe order rather than the original benchmark scene.
Restore the object's pose and clear its dynamics before every candidate attempt, not only when the helper returns.
**Knowledge Base Used:** [Simulation lab](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/simulation-lab.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| row["evaluated_cases"] = len(measured) | ||
| row["unsupported_cases"] = len(ladders) - len(measured) |
There was a problem hiding this comment.
Report counts contradict the leaderboard
The leaderboard now excludes unsupported cases from evaluated_cases, but summarize_video_recording() still counts every result as evaluated and every unsuccessful result as a failure. One successful case and one unsupported case therefore produce evaluated_cases=1 with 100% success in the leaderboard, but evaluated=2, success=1, failure=1 in the same report's notes. This makes the report harder to interpret. Report unsupported cases separately in the shared summary and use the same measured-case population as the leaderboard.
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/benchmark/atomic_action/common.py
Line: 2000-2001
Comment:
**Report counts contradict the leaderboard**
The leaderboard now excludes unsupported cases from `evaluated_cases`, but `summarize_video_recording()` still counts every result as evaluated and every unsuccessful result as a failure. One successful case and one unsupported case therefore produce `evaluated_cases=1` with 100% success in the leaderboard, but `evaluated=2, success=1, failure=1` in the same report's notes. This makes the report harder to interpret. Report unsupported cases separately in the shared summary and use the same measured-case population as the leaderboard.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
yuecideng
left a comment
There was a problem hiding this comment.
Additional inline findings that are not already covered by the existing review threads.
| ladder = StageLadder(stages=SKILL_STAGES["move_held_object"]) | ||
| if not is_success: | ||
| ladder.fail("transported", "planner_reported_failure") | ||
| elif not still_held: |
There was a problem hiding this comment.
[P1] Measure retention from the physical replay
still_held is derived from result.projected_context, which is a hypothetical state produced during side-effect-free compilation before this replay runs. MoveHeldObject preserves that symbolic held relation whenever planning succeeds, so this guard remains true even if the gripper physically loses the object. If the dropped object happens to settle within the pose tolerances below, the transported stage can pass. Determine retention/drop from replay observations at the action boundary and keep projected state diagnostic-only.
| dropped = dropped_below_support(height.min_position, height.initial_position) | ||
| ladder.record( | ||
| "held", | ||
| (not dropped) and trace.settled_position <= TASK_ROTATION_TOLERANCE_RAD, |
There was a problem hiding this comment.
[P2] Verify that the object is still held
This held stage checks only that the object never fell more than the drop margin and that its settled axis remains aligned. An object released onto the same support surface can satisfy both conditions while no longer being grasped, so AxisAlign can report full success after losing the object. The standard defines this stage as still held; add physical grasp-retention evidence (for example contact or stable hand/object relative pose) at the held boundary.
| rotation_error = abs( | ||
| abs(trace.peak_signed_displacement) - abs(case.rotate_angle_rad) | ||
| ) | ||
| ladder.record( |
There was a problem hiding this comment.
[P2] Apply the cross-cutting drop rule to Pour
The replay samples only the object's rotation, so a case that reaches the commanded angle and then drops the object during the return or terminal hold still records poured=True. BENCHMARK_STANDARD.md requires any skill holding an object to fail the current stage with object_dropped when the replay crosses the drop margin. Track object height in the same replay and include that result in this verdict.
| else: | ||
| failure_reason = "place_target_miss" | ||
| ladder.record("released", released, "release_failure") | ||
| ladder.record( |
There was a problem hiding this comment.
[P2] Score placed at the actuation boundary
place_xy_error_m is computed from final_obj_position, which replay_trajectory_for_physical_validation() returns only after its terminal hold, even though segment_end_pose was captured during replay. An object can therefore miss the target when Place stops acting, roll into tolerance during settle, and pass placed; the reverse is attributed to the wrong stage. Compute this stage from the object pose at the placing/release boundary and reserve the settled pose for stable.
Co-authored-by: matafela <chenjian@dexforce.com> Co-authored-by: Jietao Chen <61959467+chase6305@users.noreply.github.com> Co-authored-by: Jietao Chen <chenjietao@dexforce.top> Co-authored-by: Yueci Deng <dengyueci@qq.com> Co-authored-by: ACRL <angryaccelerated@qq.com>
| MICROWAVE_ASSET = "Microwave/microwave.urdf" | ||
| BUTTON_LINK_NAME = "button_link" |
There was a problem hiding this comment.
Asset migration breaks benchmark setup
The atomic Press and Twist benchmarks import these tutorial scene factories, but still look up start_button_press and power_knob_rotation. The migrated Microwave configuration uses button_joint and knob_joint instead. Both benchmarks therefore fail at joint_names.index(...) during setup, before writing a report; aggregate runs are affected too. Update both benchmark consumers alongside the factories, preferably resolving the joint from the target link.
Knowledge Base Used: Asset and dataset management
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/tutorials/atomic_action/press.py
Line: 66-67
Comment:
**Asset migration breaks benchmark setup**
The atomic Press and Twist benchmarks import these tutorial scene factories, but still look up `start_button_press` and `power_knob_rotation`. The migrated Microwave configuration uses `button_joint` and `knob_joint` instead. Both benchmarks therefore fail at `joint_names.index(...)` during setup, before writing a report; aggregate runs are affected too. Update both benchmark consumers alongside the factories, preferably resolving the joint from the target link.
**Knowledge Base Used:** [Asset and dataset management](https://app.greptile.com/dexforce/-/custom-context/knowledge-base/dexforce/embodichain/-/docs/asset-data-management.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| @pytest.mark.parametrize( | ||
| ("close_qpos", "expected_magnitude"), [(None, 0.55), (0.4, 0.4)] | ||
| ) | ||
| def test_hand_commands_cover_all_six_robotiq_joints_with_mimic_directions( | ||
| close_qpos: float | None, expected_magnitude: float | ||
| ) -> None: |
There was a problem hiding this comment.
Tutorial test coverage exceeds scope
This parametrization adds a tutorial-helper test case for the default Robotiq close command. The repository explicitly prohibits creating or expanding tests whose primary subject is scripts/tutorials/** unless tutorial coverage was requested, and no such request accompanies this change. Keep tutorial-only validation within the existing static checks, or move reusable behavior into production code and test that contract. This repository requirement must be satisfied before merging.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: tests/sim/atomic_actions/test_tutorial_utils.py
Line: 713-718
Comment:
**Tutorial test coverage exceeds scope**
This parametrization adds a tutorial-helper test case for the default Robotiq close command. The repository explicitly prohibits creating or expanding tests whose primary subject is `scripts/tutorials/**` unless tutorial coverage was requested, and no such request accompanies this change. Keep tutorial-only validation within the existing static checks, or move reusable behavior into production code and test that contract. This repository requirement must be satisfied before merging.
**Context Used:** AGENTS.md ([source](https://github.com/dexforce/embodichain/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Description
#641 is merged, so this now targets
maindirectly and carries only its owncommits.
Extends the suite from five skills to twelve, measured the way #641's standard
defines: an ordered list of stages per skill, each a scene measurement, chained
into one
success_rate.Results
--profile coverage --device cpu,--repeat 1, one environment. Full report inscripts/benchmark/atomic_action/BENCHMARK_REPORT.md, per-case tables inoutputs/benchmarks/.success_ratemove_jointsmove_end_effectorslidepresspourtwistaxis_alignopen_doorpick_upplacemove_held_objecthand_overAbout 40 % of the grasp cases are ones this embodiment cannot serve
unsupported_capabilitynow covers them: the case leaves every denominator andlowers
coverage_rate, which is whatBENCHMARK_DESIGN.mdsection 5prescribes and what the standard now states for
success_rate.q1, near the basesideapproachpick_upplacemove_held_objectBoth groups were measured, not assumed. For an object 0.18 m from the base
axis, all three distinct IK branches of the grasp pose exist and driving the arm
to each one stalls, the shoulder stopping 0.44 rad short while every other joint
tracks to 0.01 rad. For a side approach, probing one case at increasing heights,
0 of the sampled grasps are attainable at the object's own height on the ground
and 3 of 3 become attainable once lifted 0.20 m, while the top approach is
attainable at every height — the gripper is hitting the ground.
Both produce a plan that executes into thin air, and the skill's own screen
cannot see either: it accepts a candidate when IK returns a solution, which says
a configuration exists, not that the arm can get to it.
has_attainable_grasp_candidate()drives each sampled grasp in physics andmarks the case unsupported only when none of them arrives. The case set is
untouched — positions, approaches and object presets are the ones #318
shipped.
Excluding those cases raised
pick_upfrom 52.08 % to 74.19 %,placefrom37.50 % to 48.28 % and
move_held_objectfrom 27.08 % to 48.15 %, and tookgraspedandreleasedto 100 %: the cases where the skill reported a heldobject the scene did not show were all unattainable ones.
Where the remaining failures are
pick_up, all eight losses atlifted: the hand closes, the arm followsits plan, the object does not rise 4 cm. Lift over the measured cases has a
median of 0.1563 m, so these are not near misses.
place, fifteen atplaced: median XY error 0.0101 m against the 1 cmtolerance, with a tail to 0.6373 m.
move_held_object: median 0.0064 m, inside tolerance, with a tail to0.6478 m — 14 of 26 scored cases land inside 1 cm and the rest are not close.
One further case failed its PickUp precondition and is reported as an
exception.
hand_over, both cases atplacedonly: 0.1048 m and 0.0680 m against thederived 0.03 m budget, neither dropped.
open_door,open_90atgrasped: the hinge never moves.One recorded failure per skill, captured with the suite's own
--record_video --record_failed_video --video_case_limit 1:These are single-sample estimates
Everything is
--repeat 1and grasp sampling is stochastic.open_doorshowswhat that means: the same three cases, the same code, run twice, scored 100 %
and then 66.67 %, because
open_90grasped the handle on one run and not theother. Every 100 % above carries that caveat.
The replay rate is not what produced these numbers
The physical-validation replay moved from 4 to the standard's calibrated 16
physics steps per waypoint.
pick_upcoverage at both rates:success_rateIdentical in aggregate; six of 48 cases swap verdicts, three each way.
What changed since the previous revision of this branch
Rebased onto
mainwith #641 in it. The branch sat on the pre-rebase stack. Fourof its commits had reached
mainthrough #620 — their five benchmark files arenow byte-identical to
main, and replaying them would have removed theaffordance_samplingsupport that landed after the branch point. Its own copyof
BENCHMARK_STANDARD.mdis dropped as superseded, and so is its press/slidetutorial strategy fix, which is already on
main: both tutorials requeststrategy="ik_interp"there today.A case the embodiment cannot serve is no longer scored as a skill failure.
has_attainable_grasp_candidate()drives each sampled grasp in physics,pre-grasp then grasp, and stops at the first one that arrives. When none does,
the case is recorded with
unsupported_capability, leaves every denominator andlowers
coverage_rateinstead. Supported cases pay one probe; only anunsupported one pays for the whole candidate list.
build_stage_leaderboardreports
coverage_rateandunsupported_casesalongside the stage rates, andthe standard's section 1 now states the denominator.
The measurement layer now follows the standard, which had replaced all three
of the things this branch shipped:
StageLadderandSKILL_STAGESreplace the four-level ladder(
planning_success → motion_valid → execution_success → task_success).Stages are declared per skill for all fifteen; a stage is reached only when
every earlier one passed, and recording past the first failure is ignored.
REPLAY_TRACKING_TOLERANCE_RADare gone, and the taxonomy is the ninetask-level reasons rather than seventeen.
open_door's 4x replay rate stays,now justified as measurement fidelity rather than as a way past the gate.
PRIMITIVE_*/TASK_*tolerances replacePHYSICAL_PLACE_XY_TOLERANCE_M(0.10) and
PHYSICAL_MOVE_HELD_OBJECT_XYZ_TOLERANCE_M(0.12).MoveJointsandMoveEndEffectorare measured on the robot after a physicalreplay and a terminal settle. From
traj[:, -1]they reported 0.000000 radand 0.00002 m, which describes the solver; they now measure 0.002996 rad and
0.0006–0.0015 m.
Twistscores the executed end-effector rotation about the knob axis, withthe axis probed off the asset. It was failing 0/3 on the knob joint, which
over-rotates to 2.03–3.14 rad because the gripper wedges against it; that
value is now a diagnostic and the skill passes 3/3 at ≤ 0.0085 rad.
HandOver's delivery budget is the derived3 x TASK_POSITION_TOLERANCE_Minstead of a borrowed 0.12 m.
hand_is_releasedcompares the hand against the skill's own open and closed commands, and
dropped_below_supportapplies the cross-cutting drop rule to the lowestpoint of a replay.
replay_and_track_channelssamples several scalars in one replay, so a skillscored on one signal can report another; a missing actuation segment now
fails the case as
invalid_caseinstead of silently scoring the trajectory'slast waypoint, which is the rebound the helper exists to avoid.
Two defects on the aggregate
--in_processpathFound while validating this branch, both fixed here.
_make_child_args()built the child namespace from a hand-kept list of thefive original benchmarks' arguments, so every action added here raised
AttributeErroron its own case selection —door_cases,press_cases,slide_cases,twist_cases,align_cases,pour_cases,hand_over_cases—the smoke profile included. The namespace now starts from the selected module's
own
add_benchmark_args()defaults, with the aggregate run's shared settingsapplied over them.
With that fixed,
--action all --in_processreachedhand_overand failed withRobot DualUr5HandOver not found: the simulator is a singleton keyed byinstance id, and leaving the previous benchmark's scene standing makes the next
one a second instance whose planner resolves against the first benchmark's
robots.
HandOver's own teardown helper moved tocommon.release_simulation()and the dispatcher calls it after each in-process benchmark.
--action all --profile smoke --device cpu --in_processnow runs all twelve andwrites twelve reports. Neither defect touched the measurements above: every
coverage run was a direct module invocation, and both failures produced no
report rather than a wrong number.
Type of change
Screenshots
N/A — reports are Markdown tables written to
outputs/benchmarks/.Checklist
black .command to format the code base.Validation
tests/benchmark/atomic_action, 41 tests.test_measurement_standard.pycarries 13 of them. Three ofthem read
BENCHMARK_STANDARD.mdand check the shipped stage table, failuretaxonomy and tolerance constants against the document, so code and standard
cannot drift apart. The rest cover the stage bookkeeping the benchmarks depend
on: a stage after a failure is never reached, a closing stage fails the case,
out-of-order recording and non-taxonomy reasons raise, an unreached stage is
N/Arather than zero, diagnostics change no outcome, stage rates divide by thecases that reached the stage, and the two shared scene rules.
test_aggregate_dispatch.pycovers the--in_processpath: parametrized overevery registered action, it compares the child namespace against the
args.*attributes each benchmark module actually reads, checks each action keeps its
own case-selection defaults, and asserts the simulation is released between
benchmarks. Against the namespace those commits replace, it fails for all seven
added actions.
All twelve benchmarks were run at
--profile coverage; the commands are in thereport's reproducing section.
Open
move_held_objecthas a half-metre tail. Median position error is insidethe 1 cm tolerance, but 12 of 26 scored cases are not close, the worst at
0.6478 m. First thing this suite should be pointed at.
pick_uploses every one of its cases atlifted— the hand closes on areachable grasp and the object does not come up.
place's fifteenplacedfailures are the same kind of question one stage later.
press's criterion does not discriminate commanded depth. Commanding 10,20 or 30 mm all drive the button to 6.00 mm of its 6 mm stroke.
--repeat 1;open_doorscored100 % and then 66.67 % on two runs of the same code.
change. It says the arm cannot reach a grasp; it does not tell PickUp to
choose differently. Whether the skill should screen candidates by
executability is a separate question, and a bigger one: nothing in these
runs shows it picking a bad candidate when a good one existed.
push_object,coordinated_pickment,coordinated_placement.🤖 Generated with Claude Code