Skip to content

Hold every joint to its acceleration limit where the path bends, timing the bends the way MoveIt's TOTG does - #55

Open
Jepson2k wants to merge 8 commits into
mainfrom
fix/segment-accel-limits
Open

Jepson2k wants to merge 8 commits into
mainfrom
fix/segment-accel-limits

Conversation

@Jepson2k

@Jepson2k Jepson2k commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

The player plays one row per tick. Wherever the path bends between rows, the joint velocity changes within a single tick, however smoothly a profile times the path coordinate. Every profile that follows a path let joints exceed their acceleration limit at those bends:

  • _enforce_segment_limits sized its acceleration stretch from the velocity-limited segment time. That time is usually far shorter than a tick, so the check rarely stretched anything.
  • TOPP-RA's rows were never checked at all.

Peak joint acceleration as a multiple of the hard limit, with accel=0.5, so the allowed peak is 0.5× (shown as before → after):

Move TOPPRA LINEAR QUINTIC TRAPEZOID
MoveL +40x +40z from standby 3.1 → 0.5 4.2 → 0.5 2.6 → 0.5 4.2 → 0.5
Blended MoveJ chain, r=5 4.3 → 0.5 4.9 → 0.5 4.1 → 0.4 4.9 → 0.5
MoveC, r=15 mm half circle 1.8 → 0.5 1.2 → 0.5 0.3 → 0.3 1.2 → 0.5

These figures come from the dry-run plan record, whose rows are two ticks apart. At tick resolution, which is what the new test checks, every move stays within the acceleration it asks for.

Changes

  • Every profile times the path's bends, as MoveIt2's TOTG does. _BlendedPath (one degree-2 PPoly) turns each corner into a parabolic blend, sized by the tick the rows are played at. TOPP-RA times its curvature, and LINEAR, TRAPEZOID and QUINTIC sample their path coordinate on it rather than on the polyline. A MoveJ chain's path parameter is its joint-space distance, so the density of the Bézier samples doesn't create speed changes.
  • Process moves (MoveP) hold one feasible tool speed under every profile. The path-speed cap includes the bends.
  • Corner blends turn the tool at a continuous rate. A spherical quadratic through the corner orientation replaces the entry→exit geodesic. A leg that turns the tool no longer forces a step in J6's speed.
  • Requested durations are handled in one place: build(). Each profile times its fastest; the post-pass runs once on the final rows, including the seam after a wrist-turn prefix; then a longer request re-times the result through a C2 spline, so acceleration scales down smoothly. Ruckig gets the request as its minimum_duration.
  • _segment_rates is a safeguard. This forward/backward pass over the played rows guarantees the tick-level limits. Its "Extending duration" message is a WARNING when it adds more than 10%.
  • Planning cost. Cartesian constraints are evaluated vectorized over fewer gridpoints, and _segment_rates is compiled at warmup.
  • CI lint fix. The async client's robot setter takes any waldoctl Robot (ty 0.0.86), the same change as in Add the SEPTIC motion profile #54.

Timing

Every row below stays within the acceleration limit (worst 0.99× the requested acceleration).

Move First version of this PR Now
Blended MoveJ chain, TOPPRA, r = 0 / 5 / 20 / 50 1.43 / 1.50 / 1.57 / 1.62 s 1.43 / 1.43 / 1.23 / 1.11 s
Same chain, LINEAR, r = 0 / 5 / 20 / 50 1.43 / 3.53 / 2.45 / 1.89 s 1.43 / 1.43 / 1.23 / 1.11 s
Two-leg J1→J2 chain, LINEAR / TRAPEZOID / QUINTIC 3.63 / 3.63 / 2.27 s 1.00 / 1.00 / 1.02 s
MoveP from standby, TOPPRA / LINEAR / QUINTIC 1.83 / 5.09 / 8.11 s, speed varies by up to 4× in TOPPRA's corners 2.50 s each, speed held within 0.3%
MoveP whose leg turns the tool, TOPPRA 39 s (J6 speed step) 7.98 s (the same legs as a MoveL chain: 4.22 s)
MoveL from standby, TOPPRA 2.08 s, planned in 213 ms 2.07 s, planned in 34 ms

Jerk is not bounded here. TOPPRA, LINEAR, TRAPEZOID and QUINTIC have unbounded jerk by design; RUCKIG and (after #54) SEPTIC bound it on the straight joint runs they time.

Tests

All of these enter through DryRunRobotClient, and the first three fail on the first version of this PR:

  • test_blended_chain_is_no_slower_than_stops_and_respects_acceleration, parametrized over TOPPRA, LINEAR, TRAPEZOID and QUINTIC.

  • test_process_move_holds_a_feasible_tool_speed, parametrized over the same four profiles.

  • test_a_chain_along_one_line_takes_as_long_as_the_one_move: replaces test_blended_chain_is_longer_than_one_move, whose three waypoints lie on one joint-space line.

  • A test that a MoveP whose leg turns the tool plans within 2× the same legs as a MoveL chain. It fails without the rotation blend.

  • TestDryRunTimingRegressions, added after the second review round. Each test fails on 149c2ca:

    • a requested duration scales acceleration down smoothly (QUINTIC, TOPPRA, LINEAR, TRAPEZOID, RUCKIG);
    • QUINTIC paths ease acceleration in and out at both ends;
    • process-move timing doesn't depend on the sampling pitch;
    • MoveS doesn't stop where the tool's turn changes, and still turns along the geodesic;
    • a timed RUCKIG move lands on its requested tick count;
    • TOPP-RA's own rows stay within the acceleration limit at a corner.

Locally: 450 passed, 8 skipped. The one failure, test_msg_mounts_on_the_flange_not_inside_the_wrist, comes from rtree missing in the local venv; the dev extra installs it.

🤖 Generated with Claude Code

https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU

Jepson2k and others added 4 commits October 11, 2026 16:44
A septic time scaling of the path coordinate whose velocity, acceleration
and jerk are all zero at both ends, alongside the existing profiles: the
trajectory builder, the profile command, the error catalogue, the state
and streaming executors, both clients and the README, with tests. It is
what par6 moves on too, so the two backends offer the same profile set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MozxHZWkbgHcaRGMkHnJe9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
Timed as one polynomial along a cartesian path, SEPTIC sized the whole
move from its steepest row. From standby the wrist's turn off its
singularity leaves a few degrees of J4 in the path's first row, so a
40 mm MoveL planned 24 s against TOPPRA's 2 s; along a bent path its jerk
bound did not hold either. SEPTIC now runs a straight joint run only, on
the per-joint polynomial builder it shares with QUINTIC, and a cartesian
path or a blended chain is timed by TOPPRA, as with RUCKIG and as par6
does.

- VALID_PROFILES and the invalid-profile remedy come from ProfileType.
- The async client's robot setter takes any waldoctl Robot, as the
  interface declares; ty 0.0.86 refuses the narrower one.
- The limit test runs a move the jerk limit times, and the velocity test
  differentiates at the tick the rows are played at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
Played one row per tick, a bend between rows is a change of joint
velocity within a tick, however smoothly a profile times the path
coordinate. _enforce_segment_limits sized its acceleration stretch from
the velocity-limited segment time, usually far under a tick, so it
seldom stretched anything, and TOPP-RA's rows were never checked: a 40 mm
MoveL from standby ran J4 at 2.6-4.2x the hard acceleration limit and a
blended joint chain its corner at 4-5x, under every path-following
profile.

Each segment between rows now gets the shortest time, never under a
tick, that keeps every joint under its velocity limit and its velocity
change between neighbouring segments within a tick of its acceleration
limit: a forward and a backward pass over the rows, repeated until
neither lowers a rate. TOPP-RA's rows go through it too. A wrist turn
ahead of a cartesian path keeps the rest row it ends on, so a joint the
path drives back the way the turn came does not reverse within a tick.

The async client's robot setter takes any waldoctl Robot, as the
interface declares; ty 0.0.86 refuses the narrower one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
A piecewise-linear path tells TOPP-RA its second derivative is zero, so
it timed every bend as a straight run, and slowing the sampled rows
afterwards could only slow them: a sharp joint-space corner took 4.26 s
against 1.44 s for its two legs run apart. As TOTG blends its waypoints,
each corner is now a parabolic blend in the path's own parameter, no
further from the row than half the finest motor step, so the solver sees
the curvature and times it: the corner takes 1.42 s within the limits.

- Gridpoints are toppra's proposal joined with the rows, every blend's
  ends and the midpoints between, and the solution is sampled with
  ParametrizeConstAccel.
- A requested duration stretches the time-optimal solution evenly
  instead of TOPPRAsd, whose fixed speed margin clamped the narrow blends
  to a stop.
- The row pass stays as a safeguard; it now fires on 1 of 14 measured
  moves, not 10.
- A process move is slowed evenly, keeping its one tool speed, and a
  requested duration the bends overran is met by the fastest timing
  slowed evenly when it fits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
@Jepson2k Jepson2k changed the title Hold every joint to its acceleration limit as the rows are played Give TOPP-RA the path's bends and hold every joint to its acceleration limit Oct 13, 2026
Reviews found the half-step blends made TOPP-RA crawl through blended
MoveJ chains, left LINEAR/TRAPEZOID/QUINTIC to a post-pass that timed
bent paths up to 3.6x slower than feasible, and let process moves dip
at every corner.

- Blends are sized by the tick the rows are played at, and every
  along-path profile samples the blended path.
- A process move's speed cap includes the bends, so it holds one
  feasible tool speed under every profile.
- A requested duration is applied once in build(), as a uniform
  stretch of the fastest timing; the post-pass runs once on the final
  rows, prefix seam included.
- Corner blends turn the tool at a continuous rate, so a leg that
  turns the tool no longer forces a constant-speed move to crawl.
- Cartesian constraints are evaluated vectorized over fewer
  gridpoints: MoveL from standby plans in 34 ms, down from 213 ms.
- _segment_rates is compiled at warmup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
@Jepson2k Jepson2k changed the title Give TOPP-RA the path's bends and hold every joint to its acceleration limit Hold every joint to its acceleration limit where the path bends, timing the bends the way MoveIt's TOTG does Oct 13, 2026
Jepson2k and others added 2 commits October 13, 2026 23:50
A bent path under RUCKIG is timed by TOPPRA, so exempting the RUCKIG
profile from the duration stretch dropped the request: 5 s ran in 1 s.
Ruckig itself now gets the duration as its minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
…ampling

- A requested duration re-times the fastest rows through a C2 spline,
  so acceleration scales down smoothly instead of arriving in pulses.
- QUINTIC along a path eases acceleration in and out at both ends.
- Blends on densely sampled smooth paths reach half the knot interval,
  so process-move timing no longer depends on the sampling pitch.
- MoveS eases its turn rate to zero at each waypoint along the geodesic,
  so it no longer nearly stops where the tool's turn changes.
- A timed RUCKIG move lands on the requested tick count without a
  spurious extension warning.
- A blend's curvature holds at its trailing break, so TOP-PRA's own rows
  stay within the acceleration limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
Jepson2k added a commit that referenced this pull request Oct 15, 2026
The interface declares robot as a waldoctl Robot, so a setter narrowed to
parol6's Robot is an invalid override to current ty. Same change as #55.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016uVshJDKTz7nCqrMGVxVJU
SEPTIC builds its fastest profile and build() stretches it to a
requested duration, the same path QUINTIC now takes, so the
_requested_or helper and the old row-by-row duration are gone. The
duration-stretch regression test covers SEPTIC beside QUINTIC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4aXBLJyxHmGVUxK4KJqgn
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant