Retime NMG rollouts under joint velocity and acceleration limits - #714
Yuan-Xinyi wants to merge 4 commits into
Conversation
|
a1f1fad to
16c00d3
Compare
cdf4fe1 to
4dd6607
Compare
16c00d3 to
be67fa2
Compare
Follow-up to the same change, from review on #714. Retiming fits a spline through the rollout samples and resamples it, so the returned trajectory is not the one the rollout verified. Carrying the rollout's success flag across that boundary let three different failures through: - The resampled grid can step past a waypoint the rollout stopped on, so a multi-waypoint plan could report success while its returned samples never come within tolerance of a required target. Arrival is now re-checked on the returned grid with the rollout's own test. - A spline through samples the rollout clamped at a joint limit can overshoot that limit between them, since the fit sees only the sampled values. Retimed positions are now checked against the joint limits. - The shared retiming kernel returns a zero-duration result for a path whose endpoints nearly coincide, which claimed a move with no time to execute it. A row that moves in zero total time is now a failure. Also from the same review: - Reject `TIME` sampling without an explicit interval in `retime_joint_paths`. The default interval is a sample count, and reading it as seconds silently collapsed a dense path to two samples whenever its duration was shorter. - Annotate `NeuralPlannerCfg.constraints` as `dict[str, float | list[float]]` rather than a bare `dict`, per the repository's public-API requirement. Refs #684 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
All five findings were valid; fixed in Findings 1-3 share one root cause worth naming: retiming fits a spline through the rollout samples and resamples it, so the returned trajectory is not the one the rollout verified. Carrying the rollout's success flag across that boundary was the mistake, and it let all three failures through. Success is now re-derived from the samples actually returned. 1. Waypoint arrival after resampling — fixed. The resampled grid can step past a waypoint the rollout stopped on. Arrival is re-checked on the returned grid using the rollout's own 2. Zero-duration result claimed as success — fixed. The shared kernel's near-coincident-endpoint shortcut returns 3. Spline overshoot past joint limits — fixed. The fit sees only the sampled values, so a path clamped at a limit can overshoot between knots. Retimed positions are now checked against the joint limits. Not clamped: clamping would reintroduce a kink and break the acceleration bound that is the point of the feature, so the row is reported as failed instead. 4. 5. Bare |
Follow-up to the same change, from review on #714. Retiming fits a spline through the rollout samples and resamples it, so the returned trajectory is not the one the rollout verified. Carrying the rollout's success flag across that boundary let three different failures through: - The resampled grid can step past a waypoint the rollout stopped on, so a multi-waypoint plan could report success while its returned samples never come within tolerance of a required target. Arrival is now re-checked on the returned grid with the rollout's own test. - A spline through samples the rollout clamped at a joint limit can overshoot that limit between them, since the fit sees only the sampled values. Retimed positions are now checked against the joint limits. - The shared retiming kernel returns a zero-duration result for a path whose endpoints nearly coincide, which claimed a move with no time to execute it. A row that moves in zero total time is now a failure. Also from the same review: - Reject `TIME` sampling without an explicit interval in `retime_joint_paths`. The default interval is a sample count, and reading it as seconds silently collapsed a dense path to two samples whenever its duration was shorter. - Annotate `NeuralPlannerCfg.constraints` as `dict[str, float | list[float]]` rather than a bare `dict`, per the repository's public-API requirement. Refs #684 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
eb1d377 to
ff8a48e
Compare
| gridpoints=max( | ||
| _RETIME_GRIDPOINTS_MIN, | ||
| _RETIME_GRIDPOINTS_PER_SAMPLE * int(positions.shape[1]), | ||
| ), |
There was a problem hiding this comment.
With constraints enabled, a rollout that uses the default 240-step budget requests at least 24,100 TOPPRA gridpoints, compared with the kernel’s roughly 2,410-point default for that path. The shared entry point solves batch rows serially, so slow-to-converge or failed rollouts add planning latency that grows with batch size. A bounded or adaptive density would avoid paying this cost for every rollout sample.
Prompt To Fix With AI
This is a comment left during a code review.
Path: embodichain/lab/sim/motion/planners/neural_planner.py
Line: 696-699
Comment:
**Long rollouts slow retiming**
With constraints enabled, a rollout that uses the default 240-step budget requests at least 24,100 TOPPRA gridpoints, compared with the kernel’s roughly 2,410-point default for that path. The shared entry point solves batch rows serially, so slow-to-converge or failed rollouts add planning latency that grows with batch size. A bounded or adaptive density would avoid paying this cost for every rollout sample.
---
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!
… limits `NeuralPlanner` integrates `action * action_scale` per step and reports `NeuralPlannerCfg.dt` as a constant, so its timing is bookkeeping rather than something any dynamic limit informed: at the default scale a saturated step implies 20 rad/s against 2.62 rad/s for the slowest FR3 joint. The rollout is a usable geometric path, but nothing turned it into an executable trajectory. - Add `NeuralPlannerCfg.constraints`, default `None`, which preserves the current nominal-timing behavior exactly. When set, the rollout samples are re-parameterized under those velocity and acceleration limits and the returned timing, derivatives and poses come from the solved trajectory. - Expose `toppra_planner.retime_joint_paths()` for this: the time parameterization `ToppraPlanner` already owns, as a batched entry point for planners that emit geometry without timing. It selects backends as the planner's `auto` mode does -- Warp on CUDA or when a gradient is needed, NumPy otherwise -- so an NMG rollout on the GPU is retimed there, batched, and differentiably. - Recompute poses from the retimed positions. Retiming resamples along the fitted spline, so carrying `xpos_list` over from the rollout would describe samples that are no longer the ones being returned. - Report an environment whose path cannot be parameterized as failed. The caller asked for limit-respecting output and none exists for that row. - Share one result assembly between the planner and the new entry point rather than duplicating the tail-padding of unequal-length rows. Retiming bounds the derivatives but cannot smooth the path: jerk still comes from the policy, and the cost of respecting the limits appears as duration. Refs #684 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retiming fits a spline through the rollout samples and resamples it, so the returned trajectory is not the one the rollout verified. Carrying the rollout's success flag across that boundary lets failures through: - The resampled grid can step past a waypoint the rollout stopped on, so a multi-waypoint plan could report success while its returned samples never come within tolerance of a required target. Arrival is now re-checked on the returned grid with the rollout's own test. - A spline through samples the rollout clamped at a joint limit can overshoot that limit between them, since the fit sees only the sampled values. The parameterization bounds velocity and acceleration, not position, so retimed positions are now checked against the joint limits. - A row that changes pose in zero total time is reported as failed. The current parameterization gives tiny moves a positive duration, so this is a guard against a regression rather than a fix for present behavior. Also annotate `NeuralPlannerCfg.constraints` as `dict[str, float | list[float]]` rather than a bare `dict`, per the repository's public-API requirement. Refs #684 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The retimed output sample count defaulted to the rollout's own step count,
which is however many steps the policy happened to take. Sampling a trajectory
stretched by more than an order of magnitude on so few points leaves a
waypoint the rollout stopped on falling between two output samples, and the
arrival re-check then reports it missed.
Measured on a five-waypoint case against a 10 mm tolerance, worst waypoint
error by output sample count:
rollout (13 samples) 8.77 mm within tolerance
retimed, N = 13 10.55 mm outside
retimed, N = 64 8.64 mm within
retimed, N = 1024 8.64 mm unchanged from N = 64
This was measured with the previous parameterization backend, but the effect is
a property of output sample density, not of the parameterization: between 64
and 1024 samples the error does not move. The margin is what makes it fragile:
the policy stops as soon as it is barely inside tolerance, leaving about 1 mm
of slack for any resampling to consume.
Sample the solved trajectory on `NeuralPlannerCfg.dt` instead. That field was
already the assumed control period, and using it here makes the output an
execution-ready grid rather than a rollout-shaped one.
Refs #684
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After retiming, the planner recomputed every sample's pose with one FK call per time step, then did it a second time inside the waypoint re-check. For a batch of eight environments that was 618 ms of an 1188 ms plan. Compute the poses once with `Robot.compute_batch_fk`, which takes the whole (B, K, DOF) trajectory, and reuse them for the waypoint re-check, which now only maps them to the policy frame. On the Franka FR3 benchmark robot the batched result is identical to the per-step one (max difference 0.0); the post-retime checks fall from 618 ms to about 100 ms. The FK cost depends on the output sample count, not on which parameterization backend produced it. Refs #684 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
033af89 to
d65ada7
Compare
43eb4fd to
2025015
Compare
| active_idx = torch.where( | ||
| reached & (active_idx < episode_k), active_idx + 1, active_idx | ||
| ) | ||
| if bool((active_idx >= episode_k).all()): |
There was a problem hiding this comment.
Check completion on the CPU at every sample spend a lot of time.
| to_numpy = lambda limit: ( | ||
| limit.detach().cpu().numpy() if isinstance(limit, torch.Tensor) else limit | ||
| ) | ||
| results = [ |
There was a problem hiding this comment.
It solves CPU batch rows one at a time. Any better solution?
Stack
main← Replace external TOPPRA with differentiable CPU/CUDA retiming #724 ← Measure dynamic limits and mark nominal timing in the NMG benchmark #713 ← Retime NMG rollouts under joint velocity and acceleration limits #714 ← Pair a nominal and a retimed NMG arm in the benchmark #715xinyi/nmg-dynamic-limit-metricsDescription
This PR implements item 2 of #684: give
NeuralPlanneran opt-in time parameterization so its output can respect joint velocity and acceleration limits.NeuralPlannerintegratesaction * action_scaleper step and reportsNeuralPlannerCfg.dtas a constant. No dynamic limit informs that value, so the reported timing is bookkeeping: at the defaultaction_scale = 0.2anddt = 0.01, a saturated step implies 20 rad/s, against 2.62 rad/s for the slowest FR3 joint. The rollout is a usable geometric path, but nothing turned it into an executable trajectory.What changed:
NeuralPlannerCfg.constraints, defaultNone. The default preserves current behavior exactly. When set, the rollout samples are re-parameterized under the givenvelocityandaccelerationlimits, and the returned timing, derivatives and poses all come from the solved trajectory.toppra_planner.retime_joint_paths()as the shared entry point. It applies the TOPPRA planner's own parameterization to a path another planner already produced, with the same backend selection Replace external TOPPRA with differentiable CPU/CUDA retiming #724 introduces: Warp for CUDA input or when gradients are requested, NumPy workers on CPU. The rest-to-rest scalar law in_scalar_time_law.pywas the wrong tool: it would brake at every one of the hundreds of rollout samples. ([Proposal] Make joint velocity and acceleration first-class in the NMG waypoint contract #684 originally named that module; this corrects it.)NeuralPlannerCfg.dt, not on the rollout's step count (see below).Robot.compute_batch_fk), reused by the waypoint re-check. The per-step version cost 618 ms of an 1188 ms plan for a batch of eight; the batched result is identical (max difference 0.0) and takes about 100 ms.ToppraPlannerand the new entry point, instead of duplicating the tail-padding of unequal-length rows.What this does not do: retiming bounds the derivatives but cannot smooth the path. Jerk still comes from the policy, and the cost of respecting the limits shows up as duration.
Dependencies: none beyond #724.
Refs #684
Type of change
Measured evidence
Real NMG checkpoint (
swa3.onnx, K=5) on Franka FR3,smokesuite, 9 free-space cases at W=1/3/5, 3 measured trials each, on this branch with #724's backend. The same rollout is planned twice, once withconstraints=Noneand once with the FR3 limits, so the path is identical and only the timing differs.constraints=NonePer joint on one W=3 case, against each joint's own velocity limit and the uniform 10 rad/s² acceleration limit (retimed values are the planner's analytic derivatives):
Duration 0.060 s → 0.993 s; 7 samples → 101, one per
NeuralPlannerCfg.dt. Start and end positions are identical.Acceleration sits just under 1.000 on every case, never over. A time-optimal parameterization rides its binding constraint, and #724's interval bounds are conservative, so the limit holds between grid points rather than only at them. An earlier revision of this PR used the external
topprapackage, whose accelerations exceeded the limit by up to 4.5% at the default grid, and compensated with a denser grid. That workaround is gone: #724 makes it unnecessary, and planning latency falls from 581–840 ms to the range above.Latency is noisy on this machine; the nominal arm itself ranges 43–295 ms per case. Retiming adds roughly 30–130 ms per plan at B=1.
The same rollout before and after time parameterization. Position is unchanged: retiming moves the clock, not the path. The nominal acceleration flips sign between adjacent steps; the retimed one is bang-bang, with joints 1 and 7 alternating at ±10 rad/s².
Output sample count, not the parameterization, was losing waypoints
Worst waypoint error on a five-waypoint case against the 10 mm tolerance (measured with the previous backend; the effect depends on output sample density, not on the solver):
Inheriting the rollout's step count left a trajectory stretched 16× sampled on 13 points; the policy stops as soon as it is barely inside tolerance, leaving about 1 mm of slack for resampling to consume. Fixed in
97ebc8c0by sampling onNeuralPlannerCfg.dt.Batched rollouts need #726
In a batch, an environment that converges early holds its pose while the others keep rolling out, so its path ends in repeated samples. #724's waypoint cleanup keeps one of those duplicates as a knot, which reshapes the spline and adds a reversal before the goal. On the rollout above, four held samples lengthen the retimed duration from 0.993 s to 1.055 s (+6.2%), so a row's timing depends on what else is in its batch. #726 fixes this on #724's branch; with it, the held and unheld durations are equal. Single-environment plans are unaffected.
Validation
python -m pytest tests/sim/motion/ tests/compute/ tests/benchmark/motion_generation/— 1277 passed, 111 skipped, 7 deselected.black .— clean.python docs/scripts/check_api_docs.py— 2315/2315 documented.retime_joint_pathsis a new public export and is documented inpublic_api.rst; the result assembly stayed private since nothing outside the module needs it.context.py affected --base xinyi/nmg-dynamic-limit-metrics --explain—motion-planningandsimulation-system.motion-planning/planner-details.mdis updated here: the neural-adapter section states thatdtis nominal and whatconstraintschanges, and the retiming section names the shared entry point and its backend selection.simulation-systemmatched only through thetests/sim/watch path and describes no behavior this PR changes, so it needs no edit.context.py check— ok.Two existing tests in
test_neural_batched.pystubplanner.cfgwith a hand-built object; they gained the new field. No assertion changed.Checklist
black .command to format the code base.python docs/scripts/check_api_docs.py), if applicable🤖 Generated with Claude Code