Drawn wrongly

The solver was refusing a quarter of the sweep

Four numbers in this site's Jacobian had the wrong sign, from the foundation phase until now. Every picture it ever drew was correct, because a wrong derivative does not move a converged answer — it just makes Newton crawl, until the stall rule declares the position unreachable. The symptom was a sweep quietly returning fewer frames than it asked for, and no gate in the fleet has a rule against that.

Assumes Exactly right, and unbuildable.

This site’s premise is that a figure of a mechanism should be a solved configuration rather than a drawing. Every position in every figure is the output of Newton–Raphson driven to a residual of 101310^{-13}, against an analytic Jacobian — written out by hand rather than differenced, because the rank of that matrix is a quantity the site publishes and a differenced matrix has no exact rank.

Four entries of it had the wrong sign. Since the first phase.

Nothing was ever drawn incorrectly. That is the part worth an essay.

Four numbers, and the sign of each of them. The two Jacobian rows for a coupler point attached at u = 0.55, v = 0.85, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The pre-correction rows differ in exactly 4 places, and in every one of them the difference is a sign. At v = 0 all twelve entries agree, which is why nothing noticed.
Fig. 1 The two Jacobian rows for a coupler point, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The rows this site shipped for four phases differ in four places, and every difference is a sign.
Four numbers, and the sign of each of them. The two Jacobian rows for a coupler point attached at u = 0.3, v = 0.6, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The pre-correction rows differ in exactly 4 places, and in every one of them the difference is a sign. At v = 0 all twelve entries agree, which is why nothing noticed.
Fig. 2 The same four rows at a different coupler point and a different crank angle. Their signs are what the solver reads, and the row whose sign was wrong is wrong here too — the defect is in the derivation rather than in one configuration.

What the rows say

A coupler point is a point rigidly attached to a link. It is specified in the link’s own frame by two numbers: uu along the link from A to B, and vv perpendicular to it, both as fractions of the link’s length. That is the only way to attach a tracing point that survives a solve — giving it absolute coordinates would make it a free joint and hand the mechanism a degree of freedom it does not have.

The constraint is two equations:

rx=pxaxu(bxax)+v(byay)r_x = p_x - a_x - u(b_x - a_x) + v(b_y - a_y)

ry=pyayu(byay)v(bxax)r_y = p_y - a_y - u(b_y - a_y) - v(b_x - a_x)

Differentiating is a minute’s work. rx/ay=v\partial r_x/\partial a_y = -v, because aya_y appears once, in +v(byay)+v(b_y - a_y). And rx/by=+v\partial r_x/\partial b_y = +v.

The code had them the other way round. All four of the entries involving vv — two in each row — carried the opposite sign.

The error is exactly vvv \mapsto -v, which is the shape that decides everything about how it behaved.

Why it drew nothing wrong

A Jacobian is not part of the answer. It is part of the route to the answer.

Newton’s method computes a step by solving Jδ=rJ\,\delta = -r, applies it, and repeats until the residual is small enough. What makes the final position correct is that the residual is zero, and the residual is computed from the constraint equations, which were right the whole time. Whatever route Newton took, it stopped when the mechanism satisfied its own closure conditions to fourteen places.

A wrong Jacobian gives a wrong step direction, so it changes the route. It does not change the destination, because the destination is defined by a different function.

And the specific wrongness here was benign in the way that hurts most. A step computed from a slightly wrong derivative is usually still a descent direction — it still points, roughly, downhill — so the line search, which tries the full step and halves until the residual improves, kept finding an improvement. Just a smaller and smaller one.

The trace of a single failing solve, at one crank angle of the site’s own default four-bar:

  • Iteration 0: residual 1.745×1021.745 \times 10^{-2}, improving step found at a quarter of the full step, new residual 1.571×1021.571 \times 10^{-2}. Ratio 0.90.
  • Iteration 1: improving step at a sixteenth. Ratio 0.9985.
  • Iteration 2: improving step at a thirty-second. Ratio 0.9998.
  • Stall rule fires. The position is reported as not reached.

Every one of those iterations improved the residual. The solver never diverged, never produced a NaN, never took an uphill step. It simply crawled, and the rule that exists to stop it wasting time on a mechanism that genuinely cannot assemble — two consecutive iterations without meaningful progress — fired on a mechanism that could.

And an unreachable position is not drawn

Here is where the defect becomes invisible rather than merely subtle.

When solve() returns ok: false, the calling code does the right thing: it does not draw that frame. A sweep records it as unreached and moves on. That is correct behaviour and it is exactly what should happen for a crank angle a rocker cannot reach — the site’s whole treatment of non-Grashof linkages depends on being able to ask for a position and be refused.

So a figure built from a sweep had fewer frames in it than it asked for, and every frame it did have was perfect.

There is no visual signature. A coupler curve traced at 360 points and drawn at 302 of them is a coupler curve. A drag slider built from fifteen frames whose sweep dropped some of them still has fifteen frames, because the frames are chosen after the sweep. Nothing anywhere printed a complaint, because nothing had been asked a question it could answer wrongly.

How much of a sweep

The size of the loss depends entirely on how far the coupler point sits off the coupler line, which is what vv measures — and the dependence has a threshold in it.

A defect that draws nothing wrong. The same four-bar swept 360 times with the pre-correction Jacobian and with the corrected one, at eight coupler-point offsets. Corrected, every position is reached at every offset. Uncorrected: 5 of the offsets lose nothing at all, and then it loses 58, 159, 267 of 360. The picture was never wrong — a refused position is simply not drawn — so the only symptom was a sweep with fewer frames in it than it asked for.
Fig. 3 The same four-bar swept at 360 crank angles with the old Jacobian and the corrected one, at eight coupler-point offsets. Corrected, nothing is refused at any offset. Uncorrected, nothing is refused up to about 0.7, and then a quarter of the turn, and then three quarters.

At v=0v = 0 — a coupler point on the line of the coupler — the error vanishes entirely, because it is vvv \mapsto -v and vv is zero. Nothing is lost.

Up to v=0.7v = 0.7: still nothing. The Jacobian is wrong, the steps are worse than they should be, and Newton absorbs it.

At v=0.85v = 0.85: 58 of 360 crank angles refused. That is the site’s own default four-bar with a typical coupler point.

At v=1.0v = 1.0: 159. At v=1.2v = 1.2: 267 of 360 — three quarters of the turn.

The threshold is why this survived. A defect that appears only past a certain offset, on a site where most coupler points sit at modest offsets, produces intermittent losses that are individually unremarkable.

The historical behaviour in that figure is reconstructed rather than remembered: withOldAttachSigns rebuilds the wrong rows from the corrected ones, which is a two-line transformation because the error was so simple. So the figure is a measurement rather than a recollection, and it is the same helper the gate’s refusal uses — a figure that rebuilt the defect its own way could disagree with the gate about what the defect was.

A defect that draws nothing wrong. The same four-bar swept 360 times with the pre-correction Jacobian and with the corrected one, at eight coupler-point offsets. Corrected, every position is reached at every offset. Uncorrected: 2 of the offsets lose nothing at all, and then it loses 83, 174, 271 of 360. The picture was never wrong — a refused position is simply not drawn — so the only symptom was a sweep with fewer frames in it than it asked for.
Fig. 4 A different four-bar, and a coupler point at a different fraction along it. The threshold moves and the shape does not — nothing at all until the offset is large, then most of the turn.

Why nothing caught it

This is the part with something transferable in it, because the site already had what looks like the right check.

assertVelocityAgreesWithFiniteDifference compares an analytic velocity against a differenced one. It has been in the gate since the foundation and it caught a genuine sign error in that phase — the velocity solve had double-negated its right-hand side, and the check reported exactly 2, which is the signature of a flipped sign.

It cannot catch this one, and the reason is structural rather than a matter of tolerance. The analytic velocity is obtained by solving a linear system with the same Jacobian. If that Jacobian is wrong, the linear solve produces a velocity that satisfies a wrong equation — but the position it is compared against comes from Newton, which is driven to a residual of 101310^{-13} whatever route it takes. The two are compared after both have converged, and both converge to the right answer.

A wrong Jacobian makes Newton slower, not wrong. So every quantity the site prints — every position, velocity, angle, ratio, rank — was correct, and the only observable was a count of frames that nobody was counting.

The other twenty-three shared gates have the same blind spot for the same reason. svg_check asks whether a label fits and contrasts and stays inside the viewBox. figbox asks whether a figure fills its canvas. drag_check drives every slider in a real browser and asks whether it works. None of them asks whether a figure has as many frames as it asked for, because until now nobody knew that was a thing a figure could lose.

The check that would have caught it

It is one page, and it is now in the gate.

assertJacobianMatchesFiniteDifference takes each mechanism the site builds, nudges it off its solution so that no residual is identically zero, and compares every entry of the analytic Jacobian against a central difference of the residual it differentiates. The nudge matters: a row that is only correct at the answer would hide from a check performed at the answer.

On the corrected code the worst disagreement across the four-bar, a coupler four-bar with a negative offset, and a slider-crank is 3.5×1093.5 \times 10^{-9}, which is finite-difference noise. On the shipped code it was 1.7001.700, at exactly the entries above.

And the check has a refusal, because this site’s habit is that an assertion which has never rejected anything proves nothing. assertTheJacobianCheckCatchesTheOldSigns rebuilds the pre-correction rows and hands them to the same comparison, and fails if they are accepted. The thing it exists to catch is a specific, nameable thing, and it is required to still catch it.

Four numbers, and the sign of each of them. The two Jacobian rows for a coupler point attached at u = 0.55, v = 0, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The pre-correction rows are the same numbers, because the error was v ↦ −v and v is zero here. At v = 0 all twelve entries agree, which is why nothing noticed.
Fig. 5 The same two rows with the coupler point on the coupler line. The pre-correction values and the corrected ones are the same twelve numbers, because the error was v ↦ −v. This is the configuration in which the defect does not exist, and it is a substantial fraction of the figures on this site.
Four numbers, and the sign of each of them. The two Jacobian rows for a coupler point attached at u = 0.55, v = 1, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The pre-correction rows differ in exactly 4 places, and in every one of them the difference is a sign. At v = 0 all twelve entries agree, which is why nothing noticed.
Fig. 6 And with the point off the coupler line entirely, near the start of the turn. Four numbers and four signs again, and the check that would have caught the bug is the same check at every one of these settings.

What changed when it was fixed

Thirty of the site’s 240 built pages changed, and the changes fall into two kinds.

Figures gained frames. The cognate figure — three four-bars tracing one curve — gained an entire linkage. One of the three cognates has a coupler point far off its own coupler line, its sweep had been failing, and it was simply not being drawn. The figure looked complete because two linkages tracing one curve is already the point.

A published number moved by two thirds. The defect survey synthesises 1,176 four-bars through three prescribed poses and asks how many of them a machine could use, and it answers by sweeping each one and recording which poses it reaches. A pose the sweep cannot reach is recorded as a branch defect — the linkage is exactly correct and cannot get there in one piece — which is precisely what a refused solve looks like.

So the survey had been counting the solver’s refusals as the mechanism’s defects.

Corrected: 176 usable rather than 111, 810 branch defects rather than 934, and 190 order defects rather than 131. Sixty-five linkages that reach all three poses perfectly well had been reported as unable to, and another fifty-nine had been filed under the wrong defect.

Fifteen per cent, not nine. The essay that publishes that number has carried the corrected version since, with the change recorded in it rather than quietly applied.

Four numbers, and the sign of each of them. The two Jacobian rows for a coupler point attached at u = 0.42, v = 1.2, against a central difference of the residual they differentiate. The corrected rows agree everywhere. The pre-correction rows differ in exactly 4 places, and in every one of them the difference is a sign. At v = 0 all twelve entries agree, which is why nothing noticed.
Fig. 7 The same two rows at a larger offset. The four wrong entries are larger and still exactly four, because the error is a sign rather than a magnitude.

The line search was not the problem, and looked like it

Finding this took a wrong turn worth recording, because the evidence pointed somewhere else first.

The symptom that started it was a six-bar that would not sweep. Chasing it back gave a four-bar that would not sweep, and chasing that back gave the trace above: a solve taking one improving step at a quarter, one at a sixteenth, one at a thirty-second, and stalling.

That is a textbook picture of a line search misbehaving. Steps shrinking geometrically while the residual barely moves is what happens when a backtracking search is too eager to accept, or when the acceptance test is wrong, or when the stall rule is too aggressive. Three plausible fixes present themselves immediately, and all three would have “worked” in the sense of making the sweep complete: loosen the stall rule to three consecutive failures, or accept only steps that reduce the residual by a fixed fraction, or raise the iteration cap.

Each of those would have papered over a wrong derivative with a slower solver, and the survey would still have been counting refusals as branch defects — fewer of them, at a cost in build time, with the cause untouched.

What stopped it was noticing that the full Newton step increased the residual. Near a solution, with a correct Jacobian and a well-conditioned system, that should not happen: the whole point of Newton is that the full step is the good one. A full step that makes things worse at a residual of 10210^{-2} is not a line-search problem. It is a statement about the direction, and the direction comes from one place.

The general form: when a solver crawls, ask whether the direction is right before improving the search along it. A line search exists to compensate for a nonlinearity, not for a wrong derivative, and the two look identical from the outside.

Where the fix belongs

lib/mechanism.js is this site’s own, so the fix belongs here and nowhere else — and that was checked rather than assumed. A grep across all twenty fleet sites and the four shared kits finds the attach constraint in exactly one place. No other site has a solver with this construct, so there is nothing to promote and nothing else to repair.

That is the pleasant version of the fleet’s rule about where a fix goes. The unpleasant version, which this site has recorded before, is ticks() in figure-kit — a shared function that returned an empty array for a descending domain, so that two figures in the fleet carried no tick labels and no gridlines at all, at 24 out of 24 gates, because every gate asks whether a label fits and none asks whether it exists.

The two defects have the same shape and it is worth naming it: the symptom was absence. A figure with no gridlines and a sweep with missing frames are both things that are not there, and a gate written to check the properties of what is there cannot see either.

What to check for the next one

The general form of the lesson is a question worth asking of any numerical machinery, and it is not the obvious one.

The obvious question is “is the answer right”, and this site asks it constantly — two routes to every number, a closed form where one exists, a finite difference against every analytic derivative that feeds an answer.

The question that was missing is “is the machinery being asked something it can answer wrongly?” A derivative that only affects convergence speed is not being asked anything. It has no output that anybody reads. It can be arbitrarily wrong and every printed number stays correct, and the only way to test it is to compare it against something directly — which is a test with no motivation until the moment it finds something.

So the practical residue is a small rule: any quantity that is computed and not read is untested by construction. The Jacobian was such a quantity for four phases. The list of others on this site is short and now worth walking: the rank gap, which is reported and rarely inspected; the condition estimates inside the damped solve; the residual of a fit that is required only to be small. Each of those is a number the code computes, uses to make a decision, and never publishes — which is exactly the position the Jacobian was in.

The cheapest defence is the one now in the gate, and it is cheap because it needs no theory: compare the analytic thing against a numerical thing that was derived independently. A central difference of a residual knows nothing about how the derivative was written out, so agreement between the two is evidence about both. It costs one evaluation per column, it runs in under a second across every mechanism the site builds, and it would have found this on the day the code was written.

The reason it was not there is the ordinary one. A check is written when somebody suspects something, and nobody suspects a derivative that produces correct answers.

The defect’s signature generalises past this one bug and is worth carrying as a diagnostic. A wrong Jacobian does not move a converged answer, because convergence is decided by the residual and the residual is computed from the constraints rather than from their derivatives. What it moves is the number of iterations, since Newton’s step is only as good as the derivative it was built from — a wrong sign turns quadratic convergence into a crawl, and a crawl reaches the stall rule before it reaches the answer. So iteration count is the quantity that carries information about a derivative, and it is the one nobody logs. A solver that recorded its mean and worst iteration counts across a sweep would have shown a quarter of the positions taking an order of magnitude more steps than the rest — a pattern no residual could produce and no picture could show. A wrong derivative is invisible in the output and loud in the effort, which is a good argument for measuring the effort.

About the same objects

Not linked from either essay — found by the objects both name.

What links here

The 8 of 21 essays linking to this one that name the most of the same objects.

The objects this essay names

Each one links to every other essay that touches it.

Coupler pointFinite differenceJacobianKinematic solveLine searchLoop closureNewton–RaphsonVerification