A Python/PyDrake reproduction and research port of Push Anything: sampling-based contact-implicit model predictive control for non-prehensile manipulation with a Franka Panda. The repository combines local Linear Complementarity System (LCS) models, C3/C3+ trajectory optimization, candidate contact placement, and operational-space execution in simulation.
Status: active research code. The planning and simulation stack runs end-to-end, but this is not a claim of full paper-level reproduction. Stored benchmarks include successful and censored trials, and the current test baseline is not fully green.
The implementation follows the central decomposition used by Push Anything and sampling-C3:
- PyDrake plant: a Franka Panda with a spherical pusher interacts with configurable rigid objects and a table.
- Candidate sampling: the outer controller generates possible end-effector placements around the current object geometry.
- Local contact models: each relevant state/candidate is linearized into an LCS with dynamics and complementarity matrices.
- C3+ / C3 MPC: the default C3+ path solves a finite-horizon contact-implicit problem; C3 remains available as a comparison/falsification path.
- Mode selection: candidate objectives and progress logic select either a contact-rich MPC trajectory or a contact-free reposition trajectory.
- Execution: an operational-space controller tracks the selected trajectory at a faster inner cadence and applies torques to the Franka simulation.
The current default planner uses the repository's reduced end-effector-space
formulation (--r7 for historical falsification runs; it is not
the default architecture.
The measured experiment reports are organized into two tracks:
- 3D Jack Manipulation: full SE(3) translation and support-tripod reorientation with the Jack object.
- Single-Object Pushing: fixed-goal planar pushing for imported objects and the T-shaped benchmark.
See EXPERIMENTS.md for commands, measured outcomes, artifacts, and reporting limitations.
The maintained implementation covers the Push Anything reproduction stack, planar single-object pushing, C3/C3+, imported object geometries, and an experimental jack task with full orientation goals. Current research directions include a CRISP comparison study and investigation of continuous contact location. There is no CRISP implementation in this repository.
General 3D/SE(3) non-prehensile manipulation, broader cube-object studies, and GPU acceleration are roadmap items, not completed capabilities or benchmark claims. Existing experimental task/configuration branches should not be read as validated general-purpose 3D manipulation.
For each local candidate, LCSFormulator produces the discrete dynamics
and complementarity data based on the current contact geometry. C3+ introduces the slack
For vectors,
C3Solver then alternates:
- a global constrained-QP update;
- the C3+ componentwise
$(\lambda,\eta)$ projection; - consensus/dual and penalty updates;
- a final QP/trajectory extraction.
C3+ retains the consensus-ADMM scaffold introduced by Aydinoglu, Wei & Posa in Consensus Complementarity Control for Multi-Contact MPC (arXiv:2304.11259, §IV), then changes the contact representation and projection as described by Bui et al. The derivation below starts from C3 and marks the point at which C3+ intervenes.
Shared contact-implicit problem. Both algorithms optimize the same finite-horizon LCS problem:
The complementarity constraint makes the feasible set nonconvex and combinatorial: each contact component is either open with zero force or closed with zero gap velocity.
C3 consensus scaffold. C3 stacks
Here
C3+ intervention: expose the slack. C3+ augments each decision block with the complementarity slack,
The hard part of the feasible set is now only the product constraint between
where
C3+ then applies the same ADMM scaffold to the augmented variables:
Closed-form C3+ projection. Let
The
control/admm_solver.py implements this augmented loop. Its
The LCS above needs a discrete-time contact model to define what control/lcs_formulator.py, defaulting to Anitescu to match the reference
(c3/multibody/lcs_factory.cc, FormulateAnitescuContactDynamics).
The baseline: Stewart–Trinkle. The exact time-stepping model
(Stewart & Trinkle, 1996) keeps three variable groups per contact — the
normal force
Here
The Anitescu relaxation. Anitescu's convex formulation (Anitescu,
Optimization-based simulation of nonsmooth multibody dynamics, Math.
Program. 105, 2006) folds the normal direction into each friction edge.
One combined Jacobian replaces the three groups. Replicate the per-contact
friction coefficients across their four pyramid edges as
Each
What is gained and what is given up. The gain: the per-contact
conditions admit a convex time-stepping subproblem, the variable count drops
to
In the code. lcs_formulator.py builds the Stewart–Trinkle blocks
first (the gamma, lambda_n, and lambda_t rows) and, when
_contact_model == "anitescu"
(the default, matching the reference), overwrites D, E, F, H, c with the
folded formulation (lcs_formulator.py:1692-1698); the Stewart–Trinkle
path is preserved behind _contact_model == "stewart_trinkle" for
falsification. The per-pair-type friction map (mu_per_pair_type) enters
through
The candidate objective feeds the sampling-C3 dispatcher. As in receding-horizon
MPC, only the first execution interval is applied before the state and local
contact model are refreshed. See control/admm_solver.py,
control/lcs_formulator.py, control/ci_mpc_c3plus.py, and
control/sampling_c3/ for the implementation.
This section builds the Franka stack's math from the ground up: what the planner's state and input are, where they come from, and every matrix the port infers from the physics engine each tick.
The Franka Panda has seven joints, but the planner never sees them. The
default formulation (Push-Anything §IV-A, use_ee_space=True in
control/ci_mpc_c3plus.py) reduces the arm to the spherical pusher at its
end effector and plans in a small mixed robot/object state:
Define the object configuration and spatial velocity as
The Python port stores the reduced state in object-first order:
Here LCSFormulator.BOX_Q_SLOT, P_EE_SLOT, BOX_V_SLOT, and V_EE_SLOT; it
differs from the actor-first order used by the native C++ reference.
The input is the Cartesian force applied at the pusher, not joint torques:
The bound torque_limit is interpreted in newtons.
This is the key abstraction of the reduced formulation: the planner asks
"what force should the ball at the fingertip exert," and the downstream OSC
QP is responsible for finding the seven joint torques that realize that
force on the real arm. (The legacy full-plant path behind --r7 plans
joint torques
Drake evaluates the full arm/object/table plant in the standard form
where
Thus
The continuous dynamics above are nonlinear in LCSFormulator (control/lcs_formulator.py) linearizes them at the
measured state via Drake autodiff (Aydinoglu 2024, eq. 8):
For the reduced model, define the generalized configuration and velocity as
At the measured linearization point, the unconstrained generalized acceleration has the affine model
Because the reduced input channel is linear, this definition of
From the same plant context it extracts the contact geometry:
| Symbol | Shape | Meaning |
|---|---|---|
| signed gap distance per contact pair (negative = penetrating) | ||
| normal contact Jacobian; |
||
| tangential Jacobian, with four friction-pyramid edges per contact | ||
| incidence matrix that sums the four edge components at each contact | ||
| per-contact friction coefficients (a scalar configuration value is broadcast) |
The contact pairs are the pusher-vs-object faces plus the object-vs-ground
witness points, so n_c changes with the sampled candidate and the object's
pose — these matrices are re-inferred at every tick and for every candidate.
The reduced configuration-rate and velocity maps are
The corresponding reduced inertia is
Here, M_O is the object's spatial inertia and m_EE is the isotropic point
mass assigned to the pusher in the planning model. All quantities below are
evaluated at the current linearization point; asterisk superscripts are
suppressed for readability.
Under the default Anitescu contact model, friction is folded into one
combined contact Jacobian and the reduced-coordinate LCS blocks are assembled
in linearize_discrete_ee_space (control/lcs_formulator.py:2608-2697).
Partitioning the folded Jacobian by object and end-effector velocity gives
with
Reading the complementarity row as physics:
C3+ then solves, exactly as in the xArm section, the quadratic tracking
problem
config/sampling_c3_kik_t.yaml and
friends), including the final-QP contact boost on the last polish solve.
The OSC (control/osc/operational_space_controller.py) closes the gap
between the planner's fiction (a free-flying force ball) and the real arm:
it tracks the planned pusher trajectory and promotes the planner's contact
force to its QP, producing joint torques joint2 posture pin (Kp/Kd/W_joint2,
joint2_target_rad = 1.1) that kills the null-space orbit in the endgame,
and q_init_franka seeding. The planner's
The corrected protocol uses one uninterrupted manipulation session per object: the robot, object, controller state, and random-goal stream carry over across all 28 goals. Each goal has its own 600-second simulated-time limit. A session passes only if it reaches all 28 goals consecutively; failed sessions are not replaced with another seed.
The latest-model campaign reran 23 objects on August 28, 2026 and retained the
requested prior outcomes for Eraser and Gallon Milk. The combined result is
17 passing sessions out of 25 objects. Full current results and provenance
are in FIG8_CONSECUTIVE_28_LATEST_SESSIONS.csv.
| Object | Consecutive goals | Outcome | Failure cause |
|---|---|---|---|
| Letter I | 28/28 | Pass | — |
| Letter C | 28/28 | Pass | — |
| Letter R | 28/28 | Pass | — |
| Letter A | 28/28 | Pass | — |
| Letter Y | 4/28 | Fail | Near-success timeout: 0.0022 m position error and 0.1078 rad orientation error |
| Letter G | 28/28 | Pass | — |
| Letter B | 28/28 | Pass | — |
| Letter 3 | 28/28 | Pass | — |
| Letter H | 28/28 | Pass | — |
| Letter E | 4/28 | Fail | No-contact recovery loop; final errors 0.0263 m and 0.0861 rad |
| Letter S | 28/28 | Pass | — |
| Expo Box | 28/28 | Pass | — |
| Lotion | 28/28 | Pass | — |
| Wood Block | 24/28 | Fail | Planner/reposition stall; final errors 0.0840 m and 1.5641 rad |
| Tape | 2/28 | Fail | Near-success timeout: 0.0154 m and 0.1155 rad, followed by no-contact retries |
| Eraser | 0/28 | Fail | Persistent topple (retained) |
| Milk Bottle | 5/28 | Fail | No-contact recovery loop; final position error 0.1808 m |
| Clamp | 28/28 | Pass | — |
| Chicken Broth | 28/28 | Pass | — |
| Egg Carton | 3/28 | Fail | Inner workspace limit: EE radius 0.2787 m (minimum 0.280 m) |
| Book | 28/28 | Pass | — |
| Baby Toy | 28/28 | Pass | — |
| Gallon Milk | 0/28 | Fail | Outer workspace limit: EE radius 0.7543 m (maximum 0.750 m; retained) |
| Xbox | 28/28 | Pass | — |
| Push T | 28/28 | Pass | — |
The success gate remains reference-conformant: position error must be below 0.02 m and quaternion geodesic orientation error below 0.10 rad simultaneously. The primary actionable defect is a no-contact recovery loop: after an unproductive C3 segment, the dispatcher can reposition and select an equivalent ineffective contact repeatedly until timeout. Candidate failures should be invalidated, accepted reposition samples should predict contact closure and a nontrivial force, and repeated retries should force a fresh global sample set. Workspace targets and their tracked trajectories also need a 5–10 mm safety margin. Eraser requires separate non-planar topple recovery.
The job is to fetch the five tabletop scenarios from the external OIM reference, preserve their xArm model, scene assets, start/goal poses, and evaluation protocol, then run each scenario with our C3+ controller and publish comparable result artifacts. C3+ replans from the measured xArm and object state after each executed control interval until the OIM goal gate passes or the run budget ends.
The native DAIRLab integration is isolated from the modified reference checkout in a clean Git worktree:
- published branch (README, math, gate ledgers):
hdoh-ucsd/dairlib@
oim_c++_anything - local worktree:
external/oim_c++_anything(gitignored — clone the branch above if it is missing) - branch:
oim_c++_anything - baseline/design commit:
854a8afc - canonical task configuration:
examples/sampling_c3/oim_t/parameters/oim_t.yaml - maintained process diagram and validation gates:
examples/sampling_c3/oim_t/ARCHITECTURE.md
The native stack runs as three LCM processes configured by the single canonical YAML:
oim_t.yaml
├── xarm6_sim
├── xarm6_osc_controller
└── xarm6_sampling_c3_controller
Each block below consumes the previous block's output over LCM and publishes its own. One planning cycle traverses the loop once; the simulator and OSC run continuously underneath it.
-
xarm6_sim— physics (2 ms step). Input: commanded joint torques$\tau\in\mathbb{R}^6$ . Output: measured robot state$(q,\dot q)\in\mathbb{R}^6\times\mathbb{R}^6$ at 500 Hz and the object's spatial pose/velocity$({}^W\!p_O,q_{WO},{}^W\!v_O,{}^W\!\omega_O)$ at 20 Hz. Equation: Drake's rigid-body dynamics with hydroelastic/point contact —$M(q)\ddot q+C(q,\dot q)=\tau+\tau_g+J_c^\top f_c$ . -
State reduction (inside
xarm6_sampling_c3_controller). Input: the measured six-joint arm state and object spatial state. Output: the reduced planning state
The arm is collapsed to its stick-tip point p_P via forward kinematics;
the six joints never enter the optimization.
-
Contact sampling. Input: the reduced state
xand the T's exact two-box boundary. Output: a set of candidate pusher placements — points on the object perimeter with outward face normals, lifted to world coordinates at sampling height. -
LCS linearization (per candidate). Input: one candidate pusher position and the current
x. Output: a local Linear Complementarity System — matrices$(A,B,D,d,E,F,H,c)$ with$\lambda_k\in\mathbb{R}^{20}$ over a horizon$N=5$ :
-
C3+ solve (per candidate). Input: the candidate's LCS, the goal-encoding desired state
$x_d$ , and the cost matrices$(Q,R,G,U)$ . Output: an open-loop plan$\{x_k^\star,u_k^\star,\lambda_k^\star\}$ minimizing$\sum_k\lVert x_k-x_d\rVert_Q^2+\sum_k\lVert u_k\rVert_R^2$ by ADMM over consensus copies of$(\lambda,\eta)$ . -
Rollout ranking and selection. Input: every candidate's plan. Output: the single executed candidate — each plan is forward-simulated through its LCS and scored with the same quadratic error
$\sum_{k=0}^{N-1}\lVert e_k\rVert_Q^2+\lVert e_N\rVert_Q^2$ , where$e_k:=x_k-x_d$ (dynamic_rollout_cost); the minimum-cost candidate wins. -
xarm6_sampling_c3_controlleroutput — the execution plan. What the controller gives is not torques and not the raw C3 solution: it publishes one timestamped LCM trajectory (lcmt_timestamped_saved_traj) holding three time-aligned tracks sampled from the winning plan's first execution interval:-
end_effector_position_target— tip position knots$p_{\mathrm{des}}(t)\in\mathbb{R}^3$ , from the plan's state trajectory$x_k^\star$ (pushing) or from the collision-aware acquisition IK waypoints (repositioning); -
end_effector_stick_axis_target— the commanded stick axis (vertical), the orientation reference; -
end_effector_force_target— the feedforward Cartesian force$f^\star(t)\in\mathbb{R}^3$ , which is the C3+ input solution$u_k^\star$ passed through one-to-one; this is how the planned contact force reaches execution.
Only the first interval is executed before the loop replans from the measured state (receding horizon).
-
-
xarm6_osc_controller— operational-space control (500 Hz). Input: the three-track plan above and the measured$(q,\dot q)$ from the simulator. What the OSC gives: the six joint torques$\tau\in\mathbb{R}^6$ — it is the only block that talks to the motors. Each control tick solves DAIRLab's inverse-dynamics QP with decision variables$(\dot v,\tau,\lambda)$ :
with the commanded task accelerations from PD on each tracking objective:
The tracking objectives end_effector_force_target
-
Goal gate (each planning cycle).
Input: the measured object pose.
Equation measured:
$e_p=\lVert(x,y)-(x_g,y_g)\rVert_2$ and$e_\theta=\lvert\mathrm{wrap}(\theta-\theta_g)\rvert$ (see the task definition below). The run ends when both pass their tolerances simultaneously, or when the update budget is exhausted.
oim_t.yaml replaces the legacy task-level composition through
sim_params.yaml, goal_params.yaml, and
sampling_c3_controller_params.yaml. It owns the xArm model contract, OIM T
start and goal in the unwarped oim_world frame, simulation timing, success
tolerances, and LCM routing. Algorithm-specific C3+, sampling, repositioning,
progress, and OSC parameter files remain separate until their numerical
provenance has been validated for xArm.
An xArm6 with a vertical pushing stick must push a planar T block across an
open table from its start pose to a goal pose. The goal variables are the
object's planar SE(2) pose in the oim_world frame
(examples/sampling_c3/oim_t/parameters/oim_t.yaml in the C++ worktree):
where object.goal_pose and
Success is a terminal tolerance check on both goal variables simultaneously
(task.translation_tolerance, task.orientation_tolerance):
The orientation error is computed in three steps
(xarm6_full_sampling_c3plus.cc:92-107):
-
Yaw extraction. The measured object quaternion
$q_{WO}=(q_w,q_x,q_y,q_z)$ is normalized and reduced to its heading:
This is the standard ZYX yaw formula; roll and pitch are ignored by the
planar gate (a tilted or toppled T is caught by the separate settle check's
tilt angle
-
Raw difference.
$\Delta\theta:=\theta-\theta_g$ . This raw value is meaningless as a distance, because yaw lives on the circle$S^1$ , not on the real line:$\theta$ and$\theta+2\pi$ are the same physical heading, so$\Delta\theta$ can be off by any multiple of$2\pi$ depending on which branchatan2returned. -
Wrapping.
Feeding atan2 rebuilds the unique representative in
Why this matters for open_table specifically: the goal heading is
atan2 branch cut. A T
that has essentially reached the goal can be measured at
The same wrap is used everywhere a yaw difference is consumed: the terminal
gate, the per-cycle progress accounting
(xarm6_full_sampling_c3plus.cc:836-846), and the settle check's yaw_delta.
The task therefore requires roughly
Each control cycle solves a finite-horizon contact-implicit MPC problem with
C3+ (ADMM over consensus copies) on a locally linearized Linear
Complementarity System. The state is
The desired state open_table goal variables directly: the
object-position slots hold goal_pose.z() (xarm6_full_sampling_c3plus.cc:1597-1605). The cost
matrices are assembled in RunSolveAtSampledPusher
(xarm6_full_sampling_c3plus.cc:1579-1596):
so translation error is weighted at an effective 10,000 per m² on object x/y
and orientation enters through the quaternion-error terms. ADMM additionally
carries consensus and projection penalties
Around that inner QP, three more objectives shape the behavior:
-
Sample ranking. Candidate pusher placements are each solved and then
scored by forward-simulating the plan through the LCS and accumulating the
same quadratic error,
$\sum_{k=0}^{N-1}\lVert e_k\rVert_Q^2$ plus a terminal term (dynamic_rollout_cost,xarm6_full_sampling_c3plus.cc:1740-1834); the minimum-cost candidate is executed. A separateobject_yaw_cost_weight: 50.0biases goal/sample selection toward yaw progress. -
One-obstacle OIM ranking. When an OIM obstacle task supplies the
oim_obstacle_*cost keys, the exact OIM pose-ranking term is extended with$w_{\mathrm{obstacle}}\sum_k\exp(-d_k/\ell_{\mathrm{obstacle}})$ , where$d_k$ is the signed clearance from the object footprint to the nearest obstacle. The reusable implementation isoim_se2_traj_costincontrol/sampling_c3/inner_solve.py;QuadraticManipulationCostcarries the correspondingoim_obstacle_*configuration into candidate ranking. The inner C3+ QP remains quadratic; this proximity term is evaluated on the predicted rollout so obstacle avoidance does not alter the established QP weights. -
Acquisition IK. Repositioning to a sampled contact solves an inverse
kinematics problem with a position constraint on the stick tip and the
restored source tilt objective
$w_{\mathrm{tilt}}(1-\cos\psi)$ ,$w_{\mathrm{tilt}}=80$ , which keeps the stick vertical inside the feasibility band (xarm6_sampling_c3_controller.cc:120,:391-400). -
OSC tracking. The 500 Hz operational-space controller tracks the
selected trajectory with Cartesian gains
$k_p=200$ ,$k_d=20$ and a 0.01-weight joint-posture regularizer.
In short: the task asks for
Terminal open_table success has not yet been achieved. The chronological
gate records (gates 101–388: contact budgeting, admission gating,
dwell/release cycles, contact receipts, the GATE_380 direction-preserving
limiter + restored w_tilt = 80 root-cause fix, recovery admission, and
neutral-retreat fallback) live in the C++ worktree with one ledger per gate
range under examples/sampling_c3/oim_t/ and are summarized in that
worktree's README.md. The exact model provenance for the vendored T is in
examples/sampling_c3/oim_t/OIM_T_PROVENANCE.md.
Create the audited environment and run from the repository root:
conda env create -f environment.yml
conda activate push_anything_admm
# Basic configured task
python main.py pushing
# Sampling-C3 with the default outer-controller configuration
python main.py pushing --sampling-c3 --seed 0 --name pushing_seed0
# Stored T-object workflow configuration
python main.py push_t --max-time 600 \
--sampling-c3 config/sampling_c3_kik_t.yaml \
--seed 0 --name push_t_seed0Runs write results/<name>.txt and include Git/configuration metadata in the
log. Result media can be rendered separately:
scripts/make_run_video.sh push_t_seed0 --task push_tSee REPRODUCIBILITY.md before comparing or reporting experiments. It records the audited dependency versions, canonical run metadata, result-storage policy, and current limitations.
control/ C3/C3+, ADMM, LCS/LCP, costs, OSC, sampling-C3
sim/ PyDrake environment and object models
config/ tasks, controller settings, experiment variants
scripts/ launchers, diagnostics, analysis, plotting
tools/visualizer/ log parsing and result/video rendering
tests/ unit, solver, model, integration, regression tests
docs/ conformance notes, investigations, generated figures
results/ ignored working outputs and stored local campaigns
main.py CLI, simulation loop, logging, orchestration
More detailed maps are in REPOSITORY_GUIDE.md and
the local indexes under config/, scripts/, and tests/.
python -m pytest testsThe recorded cleanup baseline collected 344 tests: 292 passed, 37 failed, and
15 errored. Twenty-five non-passing nodes were blocked because the audit runner
forbids the localhost sockets that Meshcat requires; the remaining discrepancies
are classified without changing numerical expectations. See
TEST_BASELINE.md and
tests/baseline_failures.yaml.
Before changing C3/C3+, ADMM, complementarity projection, LCS construction, or MPC behavior, establish a clean numerical baseline and preserve the reported experiment configuration.
The README figures are regenerated with:
python docs/figures/generate_readme_figures.pyThe three SVGs are conceptual diagrams derived from the current source architecture. The PNG is copied from an existing measured result and is never recomputed by the README generator.
- H. Bui et al., “Push Anything: Single- and Multi-Object Pushing From First Sight with Contact-Implicit MPC,” arXiv:2510.19974, 2025.
- A. Aydinoglu, A. Wei, W.-C. Huang, and M. Posa, “Consensus Complementarity Control for Multi-Contact MPC,” IEEE Transactions on Robotics, 40, 3879–3896, 2024.
- S. Venkatesh, B. Bianchini, A. Aydinoglu, W. Yang, and M. Posa, “Approximating Global Contact-Implicit MPC via Sampling and Local Complementarity,” arXiv:2505.13350, 2025.
- Y. Li, H. Han, S. Kang, J. Ma, and H. Yang, “On the Surprising Robustness of Sequential Convex Optimization for Contact-Implicit Motion Planning,” arXiv:2502.01055, 2025. Comparison work is a research direction only.