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()