Skip to content

mdl: make Joint::clamp terminate for any finite q - #105

Open
webertob wants to merge 1 commit into
roboticslibrary:masterfrom
webertob:fix/joint-clamp-termination
Open

mdl: make Joint::clamp terminate for any finite q#105
webertob wants to merge 1 commit into
roboticslibrary:masterfrom
webertob:fix/joint-clamp-termination

Conversation

@webertob

Copy link
Copy Markdown

Joint::clamp normalises a wraparound joint by repeated subtraction:

while (q(i) > this->max(i)) { q(i) -= range; }
while (q(i) < this->min(i)) { q(i) += range; }

Neither loop has a termination guarantee. Once |q| is large enough that q - range == q in double precision the value stops changing and the loop never exits — on finite input, with an ordinary range of 2π. A range of zero does the same immediately.

Why it is reachable

JacobianInverseKinematics::solveMetric::stepJoint::stepJoint::clamp. A solve that diverges can produce a large iterate, and the loop sits below solve()s own iteration and duration checks, so neither bounds it. The calling thread spins with no error and no way to interrupt it.

We hit this embedding the library in a Unity plugin: entering play mode froze the editor with one core pinned, no exception and nothing logged. A debugger put the thread here:

rl::mdl::Joint::clamp
rl::mdl::Joint::step
rl::mdl::Metric::step
rl::mdl::JacobianInverseKinematics::solve

The change

Normalise with fmod — one step for any magnitude, cannot spin. Inputs fmod cannot express, a non-finite q or a degenerate range, fall through to the existing rl::std17::clamp, so an infinity lands on the limit and a NaN stays a NaN for the caller to notice.

For inputs where the old loop terminated the result is unchanged: both map q into [min, max] by whole multiples of the range.

Happy to adjust style or split the guard from the normalisation if you would prefer them separate.

Joint::clamp normalises a wraparound joint by repeated subtraction:

    while (q(i) > this->max(i)) { q(i) -= range; }

That has no termination guarantee. Once |q| is large enough that q - range == q
in double precision the value stops changing and the loop never exits - on
finite input, with an ordinary range of 2*pi. A range of zero does the same
immediately.

This is reachable from the iterative solvers. JacobianInverseKinematics::solve
calls Metric::step, which calls Joint::step, which calls clamp, and a solve that
diverges can produce a large iterate. The loop sits below solve()'s own
iteration and duration checks, so neither bounds it: the calling thread spins
with no error and no way out. In an application embedding the library, this
presents as a freeze with one core pinned.

Normalise with fmod instead. It is one step for any magnitude and cannot spin.
Inputs fmod cannot express - a non-finite q, or a degenerate range - fall
through to the existing clamp, so an infinity lands on the limit and a NaN stays
a NaN for the caller to notice.

The result is unchanged for inputs where the old loop terminated: both map q
into [min, max] by whole multiples of the range.
Copilot AI lite review requested due to automatic review settings August 22, 2026 13:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates rl::mdl::Joint::clamp to avoid potential non-termination when normalizing wraparound joints by replacing repeated subtraction/addition loops with a std::fmod-based normalization, and falling back to rl::std17::clamp for non-finite inputs or degenerate ranges.

Changes:

  • Add <cmath> and switch wraparound normalization to use std::fmod for constant-time wrapping.
  • Add guards to avoid fmod for non-finite q or zero/degenerate ranges, falling back to rl::std17::clamp.
  • Add explanatory in-code documentation describing the previous infinite-loop failure mode and the new behavior.
Suppressed comments (1)

src/rl/mdl/Joint.cpp:95

  • fmod(q - min, range) maps exact multiples of range to 0, which makes q(i) == max(i) (and q(i) == max(i) + k*range) normalize to min(i) instead of staying at max(i) as the previous while-loop implementation did. This is a behavioral change for continuous revolute joints (e.g., [-pi, pi]) and contradicts the PR description that results are unchanged when the old loop terminated.
					::rl::math::Real wrapped = ::std::fmod(q(i) - this->min(i), range);
				
					if (wrapped < 0)
					{
						wrapped += range;
					}
				
					q(i) = this->min(i) + wrapped;

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/rl/mdl/Joint.cpp
Comment on lines +71 to +73
::rl::math::Real range = this->wraparound(i)
? ::std::abs(this->max(i) - this->min(i))
: ::rl::math::Real(0);
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.

2 participants