The Last 10% of a Six-Bar Linkage
The moment I realized something was wrong came when I zoomed in on my animation. The mechanismâs output link was tracing a beautiful curve through five waypoints, then stopping just short of the sixthâthe one that actually mattered. After three days of work, my linkage solver was 95% of the way there, which in mechanism design means it didnât work at all.
Whatâs a Six-Bar Linkage, Anyway?
A six-bar linkage is a mechanism made of six rigid links connected by rotating joints. Think of it as a more sophisticated cousin of the four-bar linkages you see in car suspensions or folding chairs. The extra links give you more control over the output motionâyou can design paths that would be impossible with simpler mechanisms. In my case, I needed the output link (the âcouplerâ) to pass through six specific positions in space as part of a class project on advanced mechanism synthesis.
The challenge is that these six positions arenât independent wishesâtheyâre geometric constraints that the mechanism either satisfies or doesnât. Thereâs no partial credit in kinematics.
When âClose Enoughâ Isnât
The problem was deceptively simple. My optimization code would find solutions that hit all five intermediate waypoints with impressive precision. But the sixth positionâthe endpoint where the mechanism needed to finish its motionâwould get tantalizingly close and then just stop short.
Mechanical linkages donât care about your optimization algorithmâs feelings. If the geometry says the mechanism canât physically reach a position, it wonât. At certain configurations, the circles that define joint positions simply donât intersect. The math constrains whatâs possible, and no amount of clever coding can violate trigonometry.
The core solver looked clean enough:
def solve_linkage_position(params, theta2):
"""Solve for all joint positions given input angle theta2."""
A = params['A'] # Fixed ground pivot
G = params['G'] # Second fixed ground pivot
# Calculate each joint position in sequence
B = euler_to_point(A, params['L_AB'], theta2) # Input crank tip
E = solve_circle_intersection(B, params['L_EB'], G, params['L_GE'])
C = solve_triangle_point(B, E, params['L_BC'], params['L_CE'])
F = solve_triangle_point(E, G, params['L_EF'], params['L_FG'])
D = solve_circle_intersection(C, params['L_CD'], F, params['L_DF']) # Output point
return {'A': A, 'B': B, 'C': C, 'D': D, 'E': E, 'F': F, 'G': G}
Each function call solves for where two circles intersect or where a triangleâs third vertex must be given two sides and two known vertices. When any of these geometric constructions become impossibleâcircles too far apart to intersect, triangles that canât closeâthe solver fails for that input angle. Point D is the output I care about; it needs to reach all six target positions as the input angle theta2 sweeps through its range.
The Range-Finding Problem
The real issue lived in how I was determining the valid angle range for the input crank:
def find_valid_angle_range(params, start_angle=0.0, angle_step=0.01):
"""Find the valid angle range by stepping until geometry fails."""
forward_limit = start_angle
angle = start_angle + angle_step
while angle < 2 * np.pi:
try:
solve_linkage_position(params, angle)
forward_limit = angle
angle += angle_step
except: # Geometry became impossible
break
return forward_limit
This brute-force approach steps through angles until the geometry fails. Itâs not sophisticated, but it finds where the mechanism locks up. The problem was that my optimization wasnât penalizing solutions that had their valid range end before reaching the target endpoint.
The optimizer was finding linkages that looked great on paperâlow error at positions one through fiveâbut couldnât physically complete their intended motion to position six.
What Claude Code Actually Helped With
I asked Claude to help ensure the solution âalways gets close to the endpoint,â and the response was to first understand the problem space before proposing fixes. This is the pattern Iâve noticed in effective AI-assisted debugging: donât jump to solutions, map the territory first.
The conversation started with Claude examining the kinematic solver, the angle range finder, and the optimization objective function. Only after understanding how all three pieces interacted did we discuss modifications.
The fix involved adding endpoint proximity as a hard constraint rather than a soft penalty:
def is_valid_candidate(params, target_positions):
"""Reject candidates that can't reach the final target position."""
final_target = target_positions[-1]
valid_range = find_valid_angle_range(params)
# Check if mechanism can get within tolerance of endpoint
for angle in np.linspace(0, valid_range, 100):
try:
positions = solve_linkage_position(params, angle)
if distance(positions['D'], final_target) < ENDPOINT_TOLERANCE:
return True
except:
continue
return False # Can't reach endpoint; reject entirely
If a candidate linkage canât reach within a specified tolerance of the endpoint, it gets rejected entirely rather than scored poorly.
Sometimes the best optimization tweak isnât adjusting weightsâitâs changing whatâs even allowed to compete.
This reframing turned a frustrating near-miss problem into a clean filter. Bad candidates get eliminated early, and the optimizer only spends cycles on geometries that can actually complete the required motion.
Shipping the Project
The session ended with getting the project onto GitHubâ242 files changed, 16,788 insertions. My collaborator needed to review the synthesis code before our project deadline, and the animations werenât going to share themselves.
[main 773ce35] Add Python6BarLinkage project folder
242 files changed, 16788 insertions(+)
Thereâs always a temptation to spend another week âperfectingâ code before sharing it. But done beats perfect, especially when someone else is waiting to build on your work.
Takeaways
-
Describe the symptom, not the diagnosis. I said âdoesnât seem to be going all the way to the endpointâ rather than prescribing a fix. This let Claude explore the actual problem space.
-
Hard constraints need hard enforcement. When physics says something is impossible, donât try to optimize your way around it. Filter impossible candidates out entirely.
-
The optimization objective isnât always the problem. Sometimes valid ranges, constraint handling, or the search space definition is where the bug lives.
-
Ship it. 242 files sitting on your local machine help nobody.
The linkage solver isnât perfectâI already have a list of edge cases to handle. But today, it reaches its endpoint. And in mechanism design, reaching your endpoint is the whole point.