File size: 10,028 Bytes
f153e8e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 | # -*- coding: utf-8 -*-
# Original main code from: https://github.com/gokul6350/ARMv6/blob/main/main_src/inverse_k.py
# Adapted and built into an interactive Gradio App for educational purposes.
# --- MODIFIED TO INCLUDE A CODE-COPYING DROPDOWN ---
import gradio as gr
import matplotlib.pyplot as plt
import numpy as np
import math
# --- Core Inverse Kinematics Logic (Refactored and Improved) ---
def inverse_kinematics_2dof(x, y, l1, l2):
"""
Calculates the joint angles (q1, q2) for a 2-DOF robotic arm.
Args:
x (float): The x-coordinate of the end effector.
y (float): The y-coordinate of the end effector.
l1 (float): The length of the first arm segment.
l2 (float): The length of the second arm segment.
Returns:
tuple: (success, q1, q2) where q1 and q2 are in radians.
'success' is False if the point is unreachable.
"""
# Check if the target is reachable
dist_sq = x**2 + y**2
if dist_sq > (l1 + l2)**2 or dist_sq < (l1 - l2)**2:
return (False, 0, 0)
# Calculate q2 using the Law of Cosines
# The value must be clamped to [-1.0, 1.0] to avoid math errors from float precision
cos_q2_arg = (dist_sq - l1**2 - l2**2) / (2 * l1 * l2)
cos_q2_arg = np.clip(cos_q2_arg, -1.0, 1.0)
q2 = math.acos(cos_q2_arg)
# Calculate q1 using atan2 for full quadrant support
angle_to_target = math.atan2(y, x)
angle_l1_to_ee = math.atan2(l2 * math.sin(q2), l1 + l2 * math.cos(q2))
q1 = angle_to_target - angle_l1_to_ee
return (True, q1, q2)
# --- Main Simulation and Plotting Function for Gradio ---
def run_simulation(x, y, l1, l2):
"""
This function is called by Gradio. It runs the inverse kinematics,
generates a plot using Matplotlib, and returns the results.
"""
success, q1_rad, q2_rad = inverse_kinematics_2dof(x, y, l1, l2)
status_message = "Success!"
shoulder_angle_text = ""
elbow_angle_text = ""
# Create the plot using Matplotlib. This is the standard way to create
# complex visualizations for Gradio's gr.Plot component.
fig, ax = plt.subplots(figsize=(6, 6))
if not success:
status_message = f"Target ({x:.1f}, {y:.1f}) is unreachable."
ax.plot(x, y, 'rx', markersize=10, label='Unreachable Target')
ax.legend()
else:
q1_deg = math.degrees(q1_rad)
q2_deg = math.degrees(q2_rad)
joint2_x = l1 * np.cos(q1_rad)
joint2_y = l1 * np.sin(q1_rad)
end_effector_x = joint2_x + l2 * np.cos(q1_rad + q2_rad)
end_effector_y = joint2_y + l2 * np.sin(q1_rad + q2_rad)
ax.plot([0, joint2_x], [0, joint2_y], 'r-', linewidth=4, label=f'Arm 1 (L1={l1})')
ax.plot([joint2_x, end_effector_x], [joint2_y, end_effector_y], 'b-', linewidth=4, label=f'Arm 2 (L2={l2})')
ax.plot(0, 0, 'ko', markersize=10, label='Joint 1 (Shoulder)')
ax.plot(joint2_x, joint2_y, 'go', markersize=10, label='Joint 2 (Elbow)')
ax.plot(end_effector_x, end_effector_y, 'mo', markersize=10, label='End Effector')
ax.legend()
shoulder_angle_text = f"{q1_deg:.2f}°"
elbow_angle_text = f"{180.0 - q2_deg:.2f}°" # Common servo angle representation
max_reach = l1 + l2 + 2
ax.set_xlim([-max_reach, max_reach])
ax.set_ylim([-max_reach, max_reach])
ax.set_aspect('equal', adjustable='box')
ax.set_xlabel('X-axis')
ax.set_ylabel('Y-axis')
ax.set_title('2-DOF Robotic Arm Simulation')
ax.grid(True)
plt.tight_layout() # Helps fit everything nicely
# Return the Matplotlib figure to be displayed by gr.Plot, along with other values
return fig, status_message, shoulder_angle_text, elbow_angle_text
# --- Python Code Snippet for the UI ---
# This string contains the self-contained Python function for easy copying.
# It's defined here for cleanliness and then used in the Gradio UI below.
CODE_SNIPPET = '''
import math
import numpy as np
def inver_k(l1, l2, x, y):
"""
Calculates the joint angles (q1, q2) for a 2-DOF robotic arm.
This is the core inverse kinematics logic.
Args:
l1 (float): The length of the first arm segment.
l2 (float): The length of the second arm segment.
x (float): The x-coordinate of the end effector.
y (float): The y-coordinate of the end effector.
Returns:
tuple: (success, q1, q2) where q1 and q2 are in radians.
'success' is False if the point is unreachable.
Returns (False, 0, 0) on failure.
"""
# Check if the target is reachable.
# The distance from origin to target squared
dist_sq = x**2 + y**2
# Check if the target is outside the outer circle or inside the inner circle.
if dist_sq > (l1 + l2)**2 or dist_sq < (l1 - l2)**2:
return (False, 0, 0)
# Calculate q2 (elbow angle) using the Law of Cosines.
# The value must be clamped to [-1.0, 1.0] to avoid math domain errors
# from floating-point inaccuracies.
cos_q2_arg = (dist_sq - l1**2 - l2**2) / (2 * l1 * l2)
cos_q2_arg = np.clip(cos_q2_arg, -1.0, 1.0)
q2 = math.acos(cos_q2_arg)
# Calculate q1 (shoulder angle).
# q1 is the angle to the target minus the angle within the arm triangle.
angle_to_target = math.atan2(y, x)
angle_l1_to_ee = math.atan2(l2 * math.sin(q2), l1 + l2 * math.cos(q2))
q1 = angle_to_target - angle_l1_to_ee
return (True, q1, q2)
# --- Example Usage ---
# arm1_len = 12.5
# arm2_len = 12.5
# target_x = -10
# target_y = 15
#
# success, q1_rad, q2_rad = inver_k(arm1_len, arm2_len, target_x, target_y)
#
# if success:
# print(f"Success! Angles in Radians: q1={q1_rad:.4f}, q2={q2_rad:.4f}")
# print(f"Success! Angles in Degrees: q1={math.degrees(q1_rad):.2f}, q2={math.degrees(q2_rad):.2f}")
# else:
# print("The target point is unreachable.")
'''
# --- Build the Gradio Interface ---
with gr.Blocks(theme=gr.themes.Soft(), title="2-DOF Arm Simulator") as app:
gr.Markdown("# 2-DOF Robotic Arm: Inverse Kinematics Simulator")
gr.Markdown(
"This app provides an easy way to understand how a 2-DOF (two-degree-of-freedom) robotic arm works. "
"Use the sliders on the left to set the target coordinates (X, Y) and arm lengths (L1, L2). "
"The simulation on the right will update in real-time to show the required joint angles."
)
gr.Markdown("---")
with gr.Row():
with gr.Column(scale=1, min_width=300):
gr.Markdown("### Controls")
x_slider = gr.Slider(minimum=-25, maximum=0, value=-10, step=0.5, label="Target X Coordinate")
y_slider = gr.Slider(minimum=-25, maximum=25, value=15, step=0.5, label="Target Y Coordinate")
l1_slider = gr.Slider(minimum=1, maximum=15, value=12.5, step=0.5, label="Arm 1 Length (L1)")
l2_slider = gr.Slider(minimum=1, maximum=15, value=12.5, step=0.5, label="Arm 2 Length (L2)")
gr.Markdown("### Calculated Angles")
status_output = gr.Textbox(label="Status", interactive=False)
shoulder_output = gr.Textbox(label="Shoulder Angle (q1)", interactive=False)
elbow_output = gr.Textbox(label="Elbow Angle (servo-style)", interactive=False)
gr.Markdown("--- \n *Original code from [ARMv6 by gokul6350](https://github.com/gokul6350/ARMv6/blob/main/main_src/inverse_k.py)*")
with gr.Column(scale=2, min_width=500):
gr.Markdown("### Simulation")
# The gr.Plot component is a "picture frame" that displays plots
# generated by libraries like Matplotlib.
plot_output = gr.Plot()
with gr.Accordion("Understanding the Math: Inverse Kinematics Formulas", open=False):
# Using a raw string r"""...""" ensures LaTeX renders correctly without Python misinterpreting backslashes.
# Formatting is adjusted to match the source image.
gr.Markdown(
r"""
To find the joint angles ($$q_1, q_2$$) for a target position $$(x, y)$$, we use the laws of geometry.
**1. Calculate the Elbow Angle ($$q_2$$)**
We use the **Law of Cosines** on the triangle formed by the two arm links and the line from the origin to the end-effector. The distance-squared from the origin to the target is $$d^2 = x^2 + y^2$$.
$$ \cos(q_2) = \frac{x^2 + y^2 - L_1^2 - L_2^2}{2L_1L_2} $$
From this, we can find $$q_2$$:
$$ q_2 = \arccos\left(\frac{x^2 + y^2 - L_1^2 - L_2^2}{2L_1L_2}\right) $$
*Note: The angle displayed in the app for the elbow is $$180^\circ - q_2$$, which is common for servo control.*
**2. Calculate the Shoulder Angle ($$q_1$$)**
The shoulder angle $$q_1$$ is composed of two parts: the angle to the target point ($$\alpha$$) and the angle within the arm triangle ($$\beta$$).
$$ q_1 = \alpha - \beta $$
Where:
* $$\alpha$$ is the angle to the target point: `atan2(y, x)`
* $$\beta$$ is the angle between the first link and the line to the end-effector: `atan2(L2 * sin(q2), L1 + L2 * cos(q2))`
`atan2(y, x)` is used instead of `atan(y/x)` because it correctly computes the angle in all four quadrants.
"""
)
# *** NEWLY ADDED DROPDOWN FOR COPYING PYTHON CODE ***
with gr.Accordion("Copy the Core Python Function", open=False):
gr.Code(
value=CODE_SNIPPET,
language="python",
label="Python Code for inver_k(l1, l2, x, y)"
)
inputs = [x_slider, y_slider, l1_slider, l2_slider]
outputs = [plot_output, status_output, shoulder_output, elbow_output]
# Use .change for live updates as sliders move
for component in inputs:
component.change(fn=run_simulation, inputs=inputs, outputs=outputs, api_name=False)
# Use .load to run the simulation once when the app starts
app.load(fn=run_simulation, inputs=inputs, outputs=outputs, api_name=False)
if __name__ == "__main__":
app.launch()
|