L2 · DS-110

A Mechanism That Moves

Compose joints into a linkage and sweep the input through its full range to confirm the output traces the target motion path with no self-collision.

01
Challenge

Try this first — before any explanation.

A nearly-complete four-bar linkage is staged: a fixed ground, a crank (input), a coupler, and a rocker (output). The crank and coupler revolute joints are in place and a connect equality pins the rocker tip to the ground anchor, but ONE pivot is missing - the coupler->rocker revolute - so the open chain is incomplete and the loop cannot close: driving the crank does nothing downstream and the coupler and rocker hang dead. A dashed teal target path shows the arc the rocker tip SHOULD trace as the crank sweeps 0->360. Your job: close the four-bar by adding the last pivot, then sweep and confirm the tip traces the target arc with no self-collision. You have placed parts (3.1) and made single joints (3.2). Now make four parts move as ONE chain. First attempts often add the joint with the wrong pos (anchor off the ground pivot) so the loop closes crooked and the path bows away; or omit range and the linkage LOCKS at a toggle position mid-sweep; or the coupler clips the crank near 180.

The Bench

Emulate the MuJoCo-WASM four-bar in numpy: an analytic closed-loop solver places crank, coupler and rocker for each input angle, traces the rocker tip, and compares it to the target arc while checking self-collision. You add the missing pivot by setting the coupler->rocker connection and tuning the input range. The final cell is the swept-path autograder: loop closure, net DOF, path RMSE, collisions, and range covered.

B\nL_COUPLER = 60.0 # coupler B->C\nL_ROCKER = 48.0 # rocker C->D\n\n# closed-loop position solver: given crank angle, find coupler/rocker pose.\n# Returns tip position (rocker tip = C) or None if the loop cannot close.\ndef solve(theta_deg):\n th = np.radians(theta_deg)\n B = A + L_CRANK * np.array([np.cos(th), np.sin(th)])\n BD = D - B; d = np.linalg.norm(BD)\n if d > L_COUPLER + L_ROCKER or d < abs(L_COUPLER - L_ROCKER):\n return None # links can't reach: loop open / locked\n a = (L_COUPLER**2 - L_ROCKER**2 + d**2) / (2*d)\n h2 = L_COUPLER**2 - a**2\n if h2 < 0:\n return None\n h = np.sqrt(h2)\n mid = B + a*(BD/d)\n perp = np.array([-BD[1], BD[0]])/d\n C = mid + h*perp # the coupler/rocker pivot = rocker tip\n return C\n\n# target arc: the intended tip path, sampled from the well-formed mechanism.\nTARGET = {int(t): solve(t) for t in range(0, 361, 5) if solve(t) is not None}\nprint('links:', L_CRANK, L_COUPLER, L_ROCKER, ' ground pins A,D set')","label":"Four-bar geometry & target arc (given - do not edit)"},{"code":"# Sweep the input crank, capture the tip trace, flag self-collision. A collision\n# is the coupler segment B-C passing within COLL_MIN of the crank segment A-B.\nCOLL_MIN = 2.0 # mm clearance the links must keep\n\ndef seg_dist(p1, p2, q1, q2):\n # min distance between 2D segments, sampled (coarse, like a broadphase check)\n ts = np.linspace(0, 1, 12)\n best = np.inf\n for t in ts:\n a = p1 + t*(p2-p1)\n for s in ts:\n b = q1 + s*(q2-q1)\n best = min(best, np.linalg.norm(a-b))\n return best\n\ndef sweep(start, stop, steps=72):\n out = {'theta': [], 'tip': [], 'collision': [], 'closed': []}\n for th in np.linspace(start, stop, steps):\n C = solve(th)\n out['theta'].append(th)\n if C is None:\n out['closed'].append(False); out['tip'].append(None); out['collision'].append(False)\n continue\n th_r = np.radians(th)\n B = A + L_CRANK*np.array([np.cos(th_r), np.sin(th_r)])\n coll = seg_dist(A, B, B, C) < COLL_MIN and np.linalg.norm(B-C) > 1e-6\n # crank vs coupler clip is detected away from their shared pivot B:\n clip = seg_dist(A, B, B + 0.3*(C-B), C) < COLL_MIN\n out['closed'].append(True); out['tip'].append(C); out['collision'].append(clip)\n return out\n\nprint('sweep() and seg_dist() ready, COLL_MIN =', COLL_MIN, 'mm')","label":"Sweep & self-collision (given - do not edit)"},{"code":"# Add the coupler->rocker pivot and choose the input crank range.\n# pivot_at_rocker_origin=True puts the last joint at the rocker frame origin (0,0)\n# so the connect equality can close the loop to ground; False mis-anchors it.\n#\n# in the full Bench this is inside plus j_crank range=.\n\npivot_at_rocker_origin = False # <-- the coupler/rocker pivot sits at rocker origin?\ncrank_range = (0, 360) # <-- the input arc to sweep (degrees)\n\n# A mis-anchored last pivot biases the whole tip trace (loop closes crooked).\nANCHOR_BIAS = np.array([0.0, 0.0]) if pivot_at_rocker_origin else np.array([8.0, 5.0])\n\nres = sweep(crank_range[0], crank_range[1], steps=72)\nn_closed = sum(res['closed'])\nprint('swept', len(res['theta']), 'steps,', n_closed, 'closed,',\n sum(c for c in res['collision'] if c), 'collisions')","label":"YOUR JOB - close the loop & set the input range (edit this cell only)"},{"code":"# Swept-path verdict: loop closure, net DOF, path RMSE/max, collisions, range covered.\n\nclosed_all = all(res['closed'])\nn_dof = 1 if pivot_at_rocker_origin else 2 # missing/mis-anchored pivot -> 2 net DOF\n\n# path error vs target arc (only over closed, collision-free steps)\nerrs, clean = [], []\nfor th, tip, coll, cl in zip(res['theta'], res['tip'], res['collision'], res['closed']):\n if not cl or tip is None:\n continue\n key = min(TARGET, key=lambda k: abs(k - th))\n errs.append(np.linalg.norm((tip + ANCHOR_BIAS) - TARGET[key]))\n if not coll:\n clean.append(th)\npath_rmse = float(np.sqrt(np.mean(np.square(errs)))) if errs else 1e9\npath_max = float(np.max(errs)) if errs else 1e9\nn_collisions = int(sum(1 for c in res['collision'] if c))\nrange_swept = float(max(clean) - min(clean)) if clean else 0.0\n\nfails = []\nif not closed_all:\n fails.append('FAIL: loop never closes - driving j_crank leaves the rocker free (flailing). Add the coupler->rocker pivot inside the rocker body so the open chain is complete; the connect equality then closes the loop.')\nelif n_dof != 1:\n fails.append(f'FAIL: mechanism has {n_dof} net DOF - the open chain is not complete. Add the missing coupler->rocker joint at the rocker origin (the connect equality is the only loop-closing element; do not add a second ground pin).')\nelif path_rmse > 1.0 or path_max > 2.0:\n fails.append(f'FAIL: output path bows {path_rmse:.1f} mm off target (rmse). The coupler->rocker pivot pos is offset - it should sit at the rocker frame origin (0,0); the equality anchor closes the loop.')\nelif n_collisions > 0:\n bad = [round(t) for t, c in zip(res['theta'], res['collision']) if c]\n fails.append(f'FAIL: coupler clips the crank near {bad[0]} deg. Reduce j_crank range to the clean arc (try 0-175).')\nelif range_swept < 170.0:\n fails.append(f'FAIL: path matches but only over {range_swept:.0f} deg - a mechanism must work through its range. Widen j_crank range to >=170 collision-free.')\n\nprint(f'closed={closed_all} n_dof={n_dof} rmse={path_rmse:.2f}mm max={path_max:.2f}mm collisions={n_collisions} range={range_swept:.0f}deg')\nif fails:\n print(fails[0])\nelse:\n print('PASS: loop closed, 1 net DOF, on-target path, collision-free over the swept range - a mechanism that moves.')","label":"Autograder - submit"}],"intro":"Emulate the MuJoCo-WASM four-bar in numpy: an analytic closed-loop solver places crank, coupler and rocker for each input angle, traces the rocker tip, and compares it to the target arc while checking self-collision. You add the missing pivot by setting the coupler->rocker connection and tuning the input range. The final cell is the swept-path autograder: loop closure, net DOF, path RMSE, collisions, and range covered.","key":"design-simulation/a-mechanism-that-moves","kind":"python","title":"A Mechanism That Moves"}">
PYTHON · NUMPY · IN-BROWSER

A Mechanism That Moves

Emulate the MuJoCo-WASM four-bar in numpy: an analytic closed-loop solver places crank, coupler and rocker for each input angle, traces the rocker tip, and compares it to the target arc while checking self-collision. You add the missing pivot by setting the coupler->rocker connection and tuning the input range. The final cell is the swept-path autograder: loop closure, net DOF, path RMSE, collisions, and range covered.

02
Model

The idea, built visually.

One joint moves one part. But you turned the crank and the far end did nothing - the chain is broken. So how is motion supposed to TRAVEL from the part you drive to the part you care about? Link the joints and you get a kinematic chain. Constrain ONE joint - the input - and because the parts are coupled, every other joint is forced to move in lockstep. You do not drive four things; you drive one, and the geometry drives the rest. A four-bar is a CLOSED loop: ground, crank, coupler, rocker, back to ground. Open, the end-link flails - anything is possible, nothing is controlled. Close the loop and the four bars can only flex one way; that single constraint turns a floppy chain into a mechanism with a path. But a mechanism only works over a RANGE. Push too far and you hit a toggle point where the linkage locks or snaps the wrong way; or two links try to occupy the same space - collision. 'It moves' is not enough; the real claim is it moves correctly, through its whole intended range, without colliding. Add the last pivot at the right anchor, close the loop, sweep the input, and check the output against the path you wanted. Match the curve, clear the collisions, survive the full range - and you have made a machine that does what you meant.

▣ Stage animation: Cold open on the broken four-bar: the crank spins, the coupler and rocker hang limp. Four bars draw as clean lines, four pivots as blue dots; driving the crank, rotation FLOWS down the chain - each joint passing motion to the next, the coupler's path a moving ghost, the rocker swinging in response, labelled input->coupler->output. The open chain flails in warm-orange; then the fourth pivot snaps the rocker back to ground and the chain becomes a closed quadrilateral that deforms smoothly, the tip sweeping a clean arc. A FLUX still shows a crisp four-bar schematic with the coupler-point's glowing locus on navy. Sweeping 0->360, at one angle the bars line up - a toggle/dead point where the arrow stutters warm-orange - and near 180 the coupler edge kisses the crank in a collision flash. Finally on the learner's four-bar the missing pivot snaps in at the right anchor, Sweep runs, the rocker tip traces the dashed target turning solid teal as it matches; readout 'path error: 0.4 mm, collisions: 0, range: 0-360'.

03
Guided practice

Build it up, step by step.

Step A (worked, full scaffold): read a fully-solved slider-crank (the simpler cousin) mixing revolute + prismatic - crank j_in (hinge Z, range 0-360), rod j_rod (hinge Z), slider j_slide (slide along X, range 0-80, the output). Driving j_in 0->360 makes the slider reciprocate 0->80->0 along X. Callout: one input, motion propagates through j_rod to the output - revolute in, prismatic out; this is the whole idea of a chain, minimal. Step B (place the coupler->rocker pivot): the skeleton is given, you supply pos and range - this joint goes INSIDE the rocker body and hinges the rocker on the coupler; it is the last joint of the open serial chain, NOT the ground pin (the connect equality is the ground pin). Hints: (1) this pivot is where the rocker meets the coupler - the rocker body's own origin; do not put it at the ground anchor; (2) the rocker body is declared pos='60 0 0' relative to the coupler, so the coupler/rocker pivot sits at the rocker frame origin pos='0 0 0', and the connect equality closes the loop back to ground. Step C (independent, no scaffold): after the loop closes the grader reports a dead point at ~205 where the linkage toggles and the path bows off-target, plus a coupler/crank clip at ~182. No template: restrict j_crank's range to the collision-free, toggle-free arc and re-sweep until the output matches - e.g. range='0 175'. If you leave full range: output deviates 14 mm and collides at 182/205 - those are toggle and interference points; restrict the input range to the clean arc.

04
Feedback

How the Bench grades your run.

PASS WHEN PASS: the four-bar closes (loop_closed), is a single net DOF (n_dof == 1), traces the target path (path_rmse <= 1.0 mm, path_max <= 2.0 mm), is collision-free (n_collisions == 0), over a real range (range_swept >= 170 deg) - a mechanism that moves.

  • FAIL: loop never closes - driving j_crank leaves the rocker free (flailing). Add the coupler->rocker pivot inside the rocker body so the open chain is complete; the connect equality then closes the loop.
  • FAIL: mechanism has 2 net DOF - the open chain is not complete. Add the missing coupler->rocker joint (the connect equality is the only loop-closing element; do not add a second ground pin).
  • FAIL: output path bows 11.3 mm off target (rmse). The coupler->rocker pivot pos is offset - it should sit at the rocker frame origin (0,0,0); the equality anchor (90,0,0) closes the loop.
  • FAIL: linkage locks at 205 deg (toggle/dead point) - motion stops propagating. Restrict j_crank range to the clean arc (try 0-175).
  • FAIL: coupler clips the crank at step 36 (~182 deg). Reduce j_crank range or check coupler length so links clear through the sweep.
  • FAIL: path matches but only over 40 deg - a mechanism must work through its range. Widen j_crank range to >=170 collision-free.
05
Retrieve & space

Bring back what you've already mastered.

  • (3.2 DOF) Probe the closed four-bar: how many net DOF does it have, and why is it 1 even though it has three revolute joints plus the loop-closing connect equality? Re-derives the Grubler intuition.
  • (3.1 pose) The connect equality only closes the loop if its anchor coincides with the rocker tip's placed pose. Recompute that anchor point from the link placements.
  • (M2.2 constraints, spaced) Add a guard that rejects link lengths violating the Grashof condition (so the crank can't fully rotate). Which length change breaks it?
06
Mastery gate

What you must demonstrate to advance.

Submit a four-bar (or slider-crank) linkage that closes (loop_closed), is a single net DOF (n_dof == 1), traces the target path (path_rmse <= 1.0 mm, path_max <= 2.0 mm), is collision-free (n_collisions == 0), over a real range (range_swept >= 170 deg). This proves you can compose joints into a working kinematic chain, drive one input to move the rest, and validate the swept output against an intended path. Clearing this gate completes Module 3 and unlocks Module 4 (Simulating Your Device's Physics), where this very linkage gains mass, friction, and actuation.

07
Project

How this feeds your build.

This lesson is the integrative build of Module 3: it folds 3.1 (place the links correctly) and 3.2 (joint each pivot with the right DOF and axis) into one validated, moving mechanism. The artifact you submit - a parametric, jointed, swept-and-verified linkage with zero self-collision - is the direct input to the course capstone (M5): in M4 you load it into physics, in M5 you optimize it against a device spec. A learner leaving this module can take a target motion path and deliver a constrained assembly that actually traces it - the body the embodied loop will go on to sense, decide, and act with.