Pages and contents

Docs
Skills
Subagents
Lessons (pitfalls)
Research

Docs · docs/linkages.md · Markdown

Linkages

A four-bar is the simplest closed linkage: an input crank a, a coupler b, an output rocker c and the ground d between the two fixed pivots. cadagent.formulas solves where it goes from the four lengths alone. It gives the Grashof class, the coupler and output angles on either assembly, the transmission angle, and the input angles where the linkage locks. Agents call it from Python. The animator below runs the browser port of the same functions, tested against the Python on shared vectors, so it never shows a number an agent would not get.

Try it: the four-bar animator

Set the four links. The crank turns, or rocks and stops where the linkage locks, and the coupler and rocker follow on the assembly you pick. The other assembly of the same links is dashed. The dotted curve is the path of the dot on the coupler plate (the coupler curve). The chart plots the transmission angle μ over a turn of the input, with the band below 40° marked. The default is Norton's Example 4-1, a crank-rocker. Its crank turns fully, but μ falls to 26.4° at 0°, below the guideline.

Four-bar animator, before its controls load: Norton's Example 4-1 (crank 40, coupler 120, rocker 80 and ground 100 mm) on its open assembly at an input angle of 40°, where the transmission angle is 37°; a crank-rocker whose lowest transmission angle, 26.4° at 0°, is below the 40° guideline

Try these:

  • Make the crank the longest link (crank 100, coupler 80, rocker 70, ground 60). Now no link turns fully: it is a triple rocker. The input rocks and stops at ±137.9°, and the crank's circle turns rust where the links cannot close.
  • Make the coupler the shortest (crank 80, coupler 40, rocker 120, ground 100). This is a double rocker. Its input locks at 51.3° and at 125.1°, and the same links fit a mirror-image range below the ground link. The coupler turns fully only by passing into the dashed assembly at each lock; the animator keeps the assembly you chose, so here the coupler swings back.
  • Switch the assembly. Open and crossed are the two ways the same links fit together at one input angle. The transmission angle is the same for both.

How to read it

The input locks where the coupler and rocker fall into one line. There the diagonal from the crank pin to the output pivot equals b + c (stretched) or |b - c| (folded), and μ is 0. The input cannot drive past that point, and both assemblies meet there. A real linkage can leave that point on either assembly; the animator stays on the one you chose. A change-point linkage (S + L = P + Q) can lay all four links in one line at 0° or 180°, and its branches meet there too.

Scroll sideways to see the full diagram →

less: Grashof

equal or more

a or d

b or c

four lengths
S shortest · L longest
P, Q the other two

S + L
against P + Q

which link
is S?

equal: a change point,
the links can fall into line
more: a triple rocker,
no link turns fully

the input turns fully
crank-rocker (S = a)
double crank (S = d)

the input rocks
double rocker (S = b)
rocker-crank (S = c)

Diagram source
flowchart TD
    L["four lengths<br/>S shortest · L longest<br/>P, Q the other two"] --> T{"S + L<br/>against P + Q"}
    T -->|"less: Grashof"| G{"which link<br/>is S?"}
    T -->|"equal or more"| N["equal: a change point,<br/>the links can fall into line<br/>more: a triple rocker,<br/>no link turns fully"]
    G -->|"a or d"| CR["the input turns fully<br/>crank-rocker (S = a)<br/>double crank (S = d)"]
    G -->|"b or c"| DR["the input rocks<br/>double rocker (S = b)<br/>rocker-crank (S = c)"]
    classDef here fill:#fffdf5,stroke:#173d3c,color:#173d3c
    classDef near fill:#eef1e4,stroke:#6f8a80,color:#173d3c
    classDef out fill:#ffe1cc,stroke:#bd3a12,color:#173d3c
    class L,T,G here
    class N near
    class CR,DR out

The transmission angle μ is the angle between the coupler and the rocker, folded to 0–90°. Near 90° the coupler pushes the rocker round; near 0° it pushes along the rocker, and the joints bind. Norton gives 40° as the usual lowest value for a linkage that must run smoothly. That is a guideline for a drive, not a force calculation.

From Python

Any one length unit works; angles are radians, counter-clockwise from the ground link.

import math
from cadagent import formulas as fx
fx.grashof(0.040, 0.120, 0.080, 0.100)                 # kind 'grashof', inversion 'crank-rocker', crank_rotates True
fx.fourbar_angles(0.040, 0.120, 0.080, 0.100, math.radians(40))         # (theta3, theta4): 20.3° and 57.3°
fx.transmission_angle(0.040, 0.120, 0.080, 0.100, 0.0)                 # 0.4605 rad, 26.4°: the lowest
fx.fourbar_input_limits(0.100, 0.080, 0.070, 0.060)    # stretched 2.406 rad: the input stops at ±137.9°

Each function states its units, limits and source in fx.catalogue(). They are listed with the other formulas on Engineering formulas.

Sweeping a closed loop: four-bars and five-bars

cadagent.mechanical.linkages drives a closed loop through a list of inputs and reports every step. It handles a four-bar from its crank, and a five-bar (two cranks on the ground, two links meeting at a tip) from both cranks or from a tip path. Each step gives the joint angles and pins and the transmission angle. It also gives two Jacobian determinants. The direct one goes to 0 at a lock: the two links meeting at the tip fall into one line and the inputs cannot drive it. The inverse one goes to 0 when a crank and its link fall in line: the edge of reach, or a rocker's end of travel. Between samples, each event is bisected to 2⁻⁴⁸ of the step. A change of assembly is a flip.

A five-bar leg hanging from its two hips, cranks 100 mm, links 120 mm and ground 80 mm, moves its foot round an elliptical stride inside its reach: its lowest transmission angle is 53.4° and its arms stay 68.8° from in line. Beside it, a map of the two crank angles shaded by transmission angle, with the stride's path; turning both cranks symmetrically from −90° locks at θ1 = −143.1°, where the links stretch into one line

The five-bar in tools/linkage_figures.py, drawn from sweep_fivebar_tip, sweep_fivebar and fivebar_workspace. The dash-dot links are the other assembly on the same cranks. tests/test_linkages.py checks that the committed SVG matches the generator. It also checks the lock against its closed form, cos θ1 = (d − 2b) / 2a.

from cadagent.mechanical import linkages as L
r = L.sweep_fourbar(100, 80, 70, 60, range(-180, 181, 5), branch="open")   # the triple rocker above, mm
r["status"], r["summary"]["input_ranges"]      # 'fail' (18 steps cannot close), {'theta2': [-135.0, 135.0]}
[(e["kind"], e["at_deg"]) for e in r["events"]]  # lock at ±137.87°, the rocker's end of travel at -112.02°
# the figure's leg, hanging: the foot below the hips (mode=-1), knees out (working=(1, -1))
leg = L.sweep_fivebar(100, 120, 100, 120, 80, [(-90 - i, -90 + i) for i in range(0, 61, 5)], mode=-1)
leg["events"][0]                             # {'kind': 'lock', 'at_deg': {'theta1': -143.13, 'theta2': -36.87}, ...}
stride = L.sweep_fivebar_tip(100, 120, 100, 120, 80, L.ellipse_path((40, -150), (50, 22), 0, 48), working=(1, -1))
stride["status"], stride["summary"]["min_transmission_deg"]                   # 'pass', 53.4

A report's status is fail when a step cannot close, or when the loop locks or flips, at a step or between two of them (a bisected event fails whatever the step size). A tip path also fails when a crank and its link fall in line. The status is warn when the transmission angle drops under the guideline (40°). track="hold" keeps the declared assembly through a lock. "nearest" follows the pose nearest the last one and flags the flip. fivebar_workspace maps a grid of crank angles, and fivebar_reach gives each arm's annulus.

In a robot and in a rendered trace

Scroll sideways to see the full diagram →

cadagent.formulas
fivebar_point · fourbar_angles
fivebar_jacobians

mechanical.linkages
sweeps, locks, flips
pass · warn · fail

simulation.robot
LoopClosure in MJCF

mechanical.motion_trace
loops: inputs sampled,
loop-pose check

MuJoCo holds the pin
(an independent check)

the renderer people see
must close the loop

Diagram source
flowchart TD
    F["cadagent.formulas<br/>fivebar_point · fourbar_angles<br/>fivebar_jacobians"] --> S["mechanical.linkages<br/>sweeps, locks, flips<br/>pass · warn · fail"]
    S --> R["simulation.robot<br/>LoopClosure in MJCF"]
    S --> T["mechanical.motion_trace<br/>loops: inputs sampled,<br/>loop-pose check"]
    R --> M["MuJoCo holds the pin<br/>(an independent check)"]
    T --> B["the renderer people see<br/>must close the loop"]
    classDef here fill:#fffdf5,stroke:#173d3c,color:#173d3c
    classDef near fill:#eef1e4,stroke:#6f8a80,color:#173d3c
    classDef out fill:#ffe1cc,stroke:#bd3a12,color:#173d3c
    class S,R,T here
    class F near
    class M,B out

cadagent.simulation.robot.write_mjcf(..., loops=[LoopClosure(...)]) closes a loop in MuJoCo with an equality constraint, after checking that the pins meet at home. The tests settle a four-bar and a five-bar there and get the solver's angles to 1e-4°. A motion-trace contract declares a loop under loops: its inputs are sampled together and its other joints solved, and a renderer that leaves the loop open or assembles it the other way fails.

Limits

  • Rigid links on ideal pins, planar, position only. No velocity, acceleration, force, dynamics or joint clearance. A printed pin joint has play that the ideal linkage does not, so a printed linkage reaches its limits early or late and rattles near them.
  • The 40° guideline is not a load check. For the joint forces, do a force analysis or a test. CAD and closed forms do not prove fit or strength.
  • Which assembly the links take at a limit is not modelled. The animator and track="hold" keep the one you chose; "nearest" takes the nearest pose. A real linkage can leave a lock either way.
  • Sampled, not continuous. A sweep finds an event where two samples straddle it. One that starts and ends between two samples is missed, so keep the steps small next to the travel.
  • No collisions between the links. A sweep that passes can still have links that hit each other or the frame. The CAD motion gate checks a joint's sweep for collisions, but it cannot yet sweep a loop's solved poses; until it can, check the loop's extreme poses in CAD.
  • Synthesis is out of scope. Finding the lengths for a wanted motion is not done here; pylinkage does that.
What we looked at before building the animator, and the tests that keep it honest
  • pylinkage (MIT, Python): synthesis, optimisation and matplotlib animations of planar linkages. Use it to find link lengths for a wanted motion. It cannot run in a web page under the site's security policy, and its numbers would not be the ones cadagent.formulas gives agents.
  • PMKS (MIT, C#) and its web successor PMKSWeb (Worcester Polytechnic Institute, TypeScript; its repository states no licence, so it cannot be reused): full planar position, velocity and acceleration analysis for teaching. Use them when a linkage needs speeds.
  • So the animator is built, on the port that the golden vectors tie to the Python. That is why it has no dependencies and needs no script beyond the site's own.
  • Tests:
    • tests/test_formulas.py checks that every four-bar with links 1 to 6 (and a millimetre copy of each) turns fully exactly when grashof says so, and that fourbar_angles closes exactly inside fourbar_input_limits.
    • At each limit, both branches meet and μ is 0; 1e-7 rad further out the circle construction finds no position.
    • tests/webkit-formulas.test.mjs runs the port on the golden vectors: both branches every 15°, change points, and every limit with points just either side.
    • tests/website-browser.mjs runs the animator at 360 and 1366 px under the site's security policy, in Chromium and WebKit, with the same vectors run again in each browser engine.
What we looked at before building the closed-loop solver, and its tests
  • pylinkage 1.3.0 (MIT): it solves each dyad by intersecting circles, and keeps the solution nearest the last pose. track="nearest" follows the same idea. It needs Python 3.11, and it has no way to choose an assembly. It reports no Jacobian and flags no flip, so we credit it rather than depend on it.
  • SymPy physics.mechanics (BSD): it builds the symbolic loop equations, but leaves the branch to a numerical root-finder. It is not installed with cad-agent.
  • FreeCAD Assembly and its OndselSolver (LGPL-2.1+): a C++ Newton-Raphson solver that runs inside FreeCAD. It reports no singularity and no assembly mode.
  • So the solver is built, on closed forms. The five-bar is in cadagent.formulas (fivebar_point, fivebar_inputs, fivebar_jacobians). The four-bar uses the existing fourbar_angles.
  • Tests (tests/test_linkages.py):
    • Norton's Example 4-1 on both assemblies, and Freudenstein's equation at every step.
    • The rocker's ends of travel against the law of cosines, and locks equal to fourbar_input_limits to 1e-9.
    • Five-bar direct and inverse kinematics against the closed forms of Gonçalves et al., Eqs. (3)–(15). The Jacobians are checked against finite differences, and the workspace edge is checked at √((a + b)² − (d/2)²).
    • tests/test_robot.py settles both loops in MuJoCo, an independent solver.

Sources: R. L. Norton, Design of Machinery, 5th ed., McGraw-Hill, 2012, Chapters 2 (the Grashof condition and its inversions) and 4 (position analysis, the transmission angle and the toggle positions); F. Freudenstein, Approximate synthesis of four-bar linkages, Transactions of the ASME 77 (1955) 853–861; C. Gosselin and J. Angeles, Singularity analysis of closed-loop kinematic chains, IEEE Transactions on Robotics and Automation 6 (1990) 281–290 (the two Jacobians); R. S. Gonçalves, J. C. M. Carvalho and R. R. Santos, A multi-objective optimization design for parallel structures, Proceedings of COBEM 2009, Sections 2.1–2.2 (the five-bar's direct and inverse closed forms, after Liu, Wang and Zheng, Robotics and Autonomous Systems 54 (2006) 221–233).