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.