trapezoid_profile.hpp¶
TrapezoidProfile — a trapezoidal motion profile.
This header declares 3 types (9 members).
Extracted from include/shulib/control/trapezoid_profile.hpp — this page is that header's documentation, reformatted, so it cannot disagree with the code. Prose about how to think about the API lives in the user guide; worked recipes live in the cookbook; this page is the complete, mechanical list of what exists.
Contents¶
struct ProfileConstraintsmaxVelocitymaxAccelerationstruct ProfileStatepositionvelocityaccelerationclass TrapezoidProfileTrapezoidProfilesampledurationisDone
struct ProfileConstraints¶
The envelope a profile must stay inside. Bare doubles by design: the CALLER picks the distance unit and these are that unit per second and per second² — inches and radians are what a motion layer would use, though none instantiates one today (header note).
struct, declared at include/shulib/control/trapezoid_profile.hpp:32.
ProfileConstraints::maxVelocity¶
Cruise-speed cap; must be > 0 (the 0 default is unusable)
field, declared at include/shulib/control/trapezoid_profile.hpp:33.
ProfileConstraints::maxAcceleration¶
Ramp rate, used for BOTH ramps (accel == decel); must be > 0
field, declared at include/shulib/control/trapezoid_profile.hpp:34.
struct ProfileState¶
The profile's command at one instant. All three fields carry the SIGN of the move — a negative distance mirrors every one of them.
struct, declared at include/shulib/control/trapezoid_profile.hpp:39.
ProfileState::position¶
Displacement FROM THE START of the move, not a field coordinate: 0 at t <= 0 and exactly distance at t >= duration(). The caller adds its own origin.
field, declared at include/shulib/control/trapezoid_profile.hpp:42.
ProfileState::velocity¶
Signed speed; 0 at both ends, never exceeds maxVelocity
field, declared at include/shulib/control/trapezoid_profile.hpp:43.
ProfileState::acceleration¶
+aMax on the up-ramp, 0 while cruising, -aMax on the down-ramp
field, declared at include/shulib/control/trapezoid_profile.hpp:44.
class TrapezoidProfile¶
A one-axis motion plan: ramp up at maxAcceleration, cruise, ramp down to rest exactly on target — degrading to a triangle when the move is too short to reach cruise speed. Built once per move and then IMMUTABLE: sample(t) is a pure function of t, so the same t always returns the same state, re-sampling is free, and nothing advances a baseline. It has no consumer in this library yet — the shipped motions servo the live error with Pid + Feedforward and generate no profile. Shipped, tested, and waiting for the profiled-motion work; the header note says why that is written down rather than implied.
class, declared at include/shulib/control/trapezoid_profile.hpp:54.
TrapezoidProfile::TrapezoidProfile¶
Plan a move of SIGNED distance under c. distance and both constraints must be FINITE, and the constraints strictly positive; a violation trips SHULIB_PRECONDITION being clamped, because a silently corrected limit is a plan nobody asked for. The finiteness of the constraints used to be unchecked, and > 0.0 is satisfied by infinity: maxAcceleration = inf was stored raw as aMax_ and handed straight back out of sample() as a non-finite acceleration target. If the move is too short to reach c.maxVelocity the plan degrades to a TRIANGLE (peak speed sqrt(|distance| * maxAcceleration), no cruise phase). A zero distance is legal and yields duration() == 0 — an already-finished plan, not an error.
function, declared at include/shulib/control/trapezoid_profile.hpp:65.
TrapezoidProfile::sample¶
The target state at t SECONDS AFTER THE MOVE STARTED — the caller owns the clock and the elapsed-time subtraction. t is CLAMPED, never rejected: t <= 0 returns rest at the start with acceleration already at ±aMax (the next instant is the up-ramp; 0 for a zero-distance move), and t >= duration() returns rest exactly on target, forever. Const and side-effect-free. A NON-FINITE t is REJECTED, not clamped — the one input that is a caller bug rather than a position on the plan's timeline. It used to fall through every comparison (each is false against NaN) into the decelerate branch and return a PARTIALLY finite state: position and velocity NaN, but acceleration a perfectly finite -aMax. A caller screening only acceleration passed it and forwarded a plausible-looking down-ramp downstream, which is the "plausible instead of visible" failure this library rejects everywhere else.
function, declared at include/shulib/control/trapezoid_profile.hpp:103.
TrapezoidProfile::duration¶
Total planned time in seconds, both ramps included (0 for a zero-distance move). This is the PLAN's time, not a promise the drivetrain tracks it — a follower's timeout must allow slack beyond this, not equal it.
function, declared at include/shulib/control/trapezoid_profile.hpp:127.
TrapezoidProfile::isDone¶
True once t has reached duration(), inclusive — i.e. sample(t) has stopped changing. True at t == 0 for a zero-distance move. A statement about the PLAN's clock only: it says nothing about whether the robot actually arrived, which is SettledUtil's question, measured against the real estimate. A non-finite t is rejected here too, on the same rule as sample(). It used to return FALSE (every NaN comparison is false), so a follower loop terminating on isDone() would spin forever on a NaN clock instead of failing fast. This member is deliberately NOT noexcept, because the precondition handler throws and a noexcept frame would turn a caller bug into std::terminate; the drop is a breaking signature change by version.hpp's rule, taken because this class has no consumer in the tree but its own test and a hang is the worse failure.
function, declared at include/shulib/control/trapezoid_profile.hpp:141.
Design commentary, from the header¶
The header opens with the reasoning behind these shapes. It is reproduced here in full because a reference that only lists signatures teaches nobody why.
The header’s own reasoning — 20 lines
TrapezoidProfile — a trapezoidal motion profile (master plan §M2). For a move of signed
`distance` under (maxVelocity, maxAcceleration), it gives the (position, velocity,
acceleration) target at any time: ramp up at aMax to the cruise speed, cruise, ramp down
to rest. If the move is too short to reach maxVelocity it degrades to a TRIANGLE (peak
speed < maxVelocity, no cruise). (S-curve is a later sibling.)
NOTHING IN THE TREE INSTANTIATES ONE YET, and this note is here because the header used
to say the opposite. It claimed "the velocity target feeds Feedforward; the position
target feeds the per-axis Pid" and "the motion layer instantiates one per axis" — both
describe an INTENDED wiring, not a real one. The C1 motions run per-axis Pid plus
Feedforward directly against the live error, with the speed cap applied in the command
pipeline; no profile is generated anywhere, and the only consumer of this class in the
repository is its own test. That is a gap, not a bug — profiled motion is future work —
but a header that describes an integration it does not have is a lie a generated
reference then publishes, so it says the true thing now.
Bare doubles, like the rest of control: the CALLER picks the distance unit and supplies
matching unit/s and unit/s² (inches and radians are what a motion layer would use).
sample(t) clamps t to [0, duration] — see its own note for what NaN does.
Last updated 2026-08-15 — published from the main release branch, commit fa545ff. This site tracks releases, so between them it can lag the repository — that lag is deliberate, and it is what keeps the development log off the public site. For the current state of the code, see the repository.