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