Four-bar linkage and its coupler curve

The example is a crank-rocker four-bar linkage with a ground d = 4, a crank a = 1, a coupler b = 3.5 and a rocker c = 3. For every crank angle, the linkage is a small sketch. The crank goes to its end, the coupler goes from there to the point where it meets the rocker’s circle, and the rocker goes back to its pivot. An arm of 2.5 at 30° to the coupler carries the coupler point. Its path, the coupler curve, comes out of 72 sketches, with no constraint solver involved.

A crank-rocker four-bar linkage running through its positions

[1]:
import math
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
import planex as px

Check: the lengths meet Grashof’s condition for a crank-rocker, with the crank shortest, so the crank turns all the way round:

[3]:
d = 4.0
a = 1.0
b = 3.5
c = 3.0
print("crank-rocker", a + d <= b + c and a < min(b, c, d))
crank-rocker True

One position

The coupler ends where the circle of its length around the crank end meets the circle of the rocker’s length around its pivot. Of the two intersections, the upper one is the intended assembly. The arm leaves the crank end at 30° to the coupler (Figure 1).

[4]:
def position(theta):
    B = (a * math.cos(math.radians(theta)), a * math.sin(math.radians(theta)))
    C = max(px.Circle(B, b).Intersections(px.Circle((d, 0), c)), key=lambda p: p.y)
    s = px.QuickSketch()
    s.Line(B, id="crank").Line((C.x, C.y), id="coupler").Line((d, 0), id="rocker")
    s.Move(B).Line(2.5, angle=(30, "coupler"), id="arm")
    return s, B, (C.x, C.y), s.pen.position


sketch, B, C, P = position(60)
Crank, coupler, rocker and the arm at 60°
Figure 1: One position of the linkage

Check

Check: the rocker angle agrees with Freudenstein’s equation, solved in closed form, over a full turn of the crank in steps of 5°:

[6]:
def freudenstein(theta):
    t = math.radians(theta)
    K1 = d / a
    K2 = d / c
    K3 = (a**2 - b**2 + c**2 + d**2) / (2 * a * c)
    A = math.cos(t) - K1 - K2 * math.cos(t) + K3
    B = -2 * math.sin(t)
    C = K1 - (K2 + 1) * math.cos(t) + K3
    return [2 * math.degrees(math.atan((-B + sign * math.sqrt(B**2 - 4 * A * C)) / (2 * A))) for sign in (1, -1)]


thetas = list(range(0, 360, 5))
positions = [position(theta) for theta in thetas]
rocker_angles = [math.degrees(math.atan2(C[1], C[0] - d)) for s, B, C, P in positions]
worst = max(min(abs((phi - f + 180) % 360 - 180) for f in freudenstein(theta))
            for theta, phi in zip(thetas, rocker_angles))
print("largest difference of the rocker angle", worst, "degrees")
largest difference of the rocker angle 2.842170943040401e-14 degrees

In motion

The animation shows the linkage through all 72 positions, with the coupler curve.

[7]:

Torque out over torque in

Without losses, the power going in at the crank comes out at the rocker. The torque ratio is therefore the inverse ratio of the angular speeds, dθ/dφ, here taken from the sketched rocker angles. It grows without bound where the rocker reverses, at the toggle positions, where a small torque on the crank holds a large one on the rocker. The change of the rocker angle over two steps of the crank, 10°, gives the ratio:

[8]:
ratio = []
for k in range(len(thetas)):
    dphi = (rocker_angles[(k + 1) % len(thetas)] - rocker_angles[k - 1] + 180) % 360 - 180
    ratio.append(10 / dphi if abs(dphi) > 1e-9 else float("inf"))

The torque ratio over a turn of the crank: