Skip to content

Commit d0f6517

Browse files
committed
Testing IK implementations
1 parent 2863beb commit d0f6517

3 files changed

Lines changed: 532 additions & 181 deletions

File tree

tests/openarm_jacobian_ik.py

Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
#!/usr/bin/env python3
2+
"""Custom Jacobian-based IK solver for OpenArm."""
3+
4+
import numpy as np
5+
from typing import Optional, Tuple
6+
7+
8+
class OpenArmJacobianIK:
9+
"""Robust IK solver using numerical Jacobian and damped least squares."""
10+
11+
def __init__(self, urdf_path: Optional[str] = None):
12+
"""Initialize the IK solver.
13+
14+
Args:
15+
urdf_path: Path to URDF file (optional, uses default if None)
16+
"""
17+
if urdf_path is None:
18+
from openarm.kinematics.models import OPENARM_URDF_PATH
19+
urdf_path = OPENARM_URDF_PATH
20+
21+
# Use ikpy only for FK (which works fine)
22+
import ikpy.chain
23+
24+
# Right arm chain
25+
active_links_mask = [False] + [True]*7 + [False]*4
26+
self._right_chain = ikpy.chain.Chain.from_urdf_file(
27+
urdf_path,
28+
base_elements=["openarm_right_link0"],
29+
last_link_vector=[0, 0, 0.08],
30+
name="openarm_right_arm",
31+
active_links_mask=active_links_mask,
32+
)
33+
34+
# Left arm chain
35+
self._left_chain = ikpy.chain.Chain.from_urdf_file(
36+
urdf_path,
37+
base_elements=["openarm_left_link0"],
38+
last_link_vector=[0, 0, 0.08],
39+
name="openarm_left_arm",
40+
active_links_mask=active_links_mask,
41+
)
42+
43+
# Joint limits (radians)
44+
self.joint_limits = np.array([
45+
[-3.490659, 1.396263], # J1
46+
[-1.745329, 1.745329], # J2
47+
[-1.570796, 1.570796], # J3
48+
[0.0, 2.443461], # J4
49+
[-1.570796, 1.570796], # J5
50+
[-0.785398, 0.785398], # J6
51+
[-1.570796, 1.570796], # J7
52+
])
53+
54+
def _forward_kinematics(self, chain, joint_angles: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
55+
"""Compute forward kinematics.
56+
57+
Args:
58+
chain: IKPy chain
59+
joint_angles: 7 joint angles in radians
60+
61+
Returns:
62+
(position, rotation_matrix)
63+
"""
64+
config_full = np.zeros(12)
65+
config_full[1:8] = joint_angles
66+
67+
fk_matrix = chain.forward_kinematics(config_full)
68+
position = fk_matrix[:3, 3]
69+
rotation = fk_matrix[:3, :3]
70+
71+
return position, rotation
72+
73+
def _compute_jacobian(self, chain, joint_angles: np.ndarray, epsilon: float = 1e-5) -> np.ndarray:
74+
"""Compute numerical Jacobian (position only, 3x7).
75+
76+
Args:
77+
chain: IKPy chain
78+
joint_angles: Current 7 joint angles
79+
epsilon: Small perturbation for numerical derivative
80+
81+
Returns:
82+
3x7 Jacobian matrix (dx/dq, dy/dq, dz/dq)
83+
"""
84+
jacobian = np.zeros((3, 7))
85+
86+
# Current position
87+
pos_current, _ = self._forward_kinematics(chain, joint_angles)
88+
89+
# Numerical derivative for each joint
90+
for i in range(7):
91+
# Perturb joint i
92+
joints_perturbed = joint_angles.copy()
93+
joints_perturbed[i] += epsilon
94+
95+
# Compute perturbed position
96+
pos_perturbed, _ = self._forward_kinematics(chain, joints_perturbed)
97+
98+
# Derivative: (pos_perturbed - pos_current) / epsilon
99+
jacobian[:, i] = (pos_perturbed - pos_current) / epsilon
100+
101+
return jacobian
102+
103+
def _clamp_joints(self, joint_angles: np.ndarray) -> np.ndarray:
104+
"""Clamp joint angles to limits.
105+
106+
Args:
107+
joint_angles: 7 joint angles
108+
109+
Returns:
110+
Clamped joint angles
111+
"""
112+
clamped = joint_angles.copy()
113+
for i in range(7):
114+
clamped[i] = np.clip(
115+
clamped[i],
116+
self.joint_limits[i, 0],
117+
self.joint_limits[i, 1]
118+
)
119+
return clamped
120+
121+
def solve(
122+
self,
123+
chain,
124+
target_position: np.ndarray,
125+
initial_joints: np.ndarray,
126+
target_orientation: Optional[np.ndarray] = None,
127+
max_iterations: int = 200, # Increased from 100
128+
position_tolerance: float = 0.001, # 1mm
129+
step_size: float = 0.3, # Reduced from 0.5 for stability
130+
damping: float = 0.05, # Increased damping for near-singularities
131+
) -> Tuple[np.ndarray, bool, float]:
132+
"""Solve IK using damped least squares.
133+
134+
Args:
135+
chain: IKPy chain (left or right)
136+
target_position: Target [x, y, z] position
137+
initial_joints: Initial 7 joint angles (starting guess)
138+
target_orientation: Optional target rotation matrix (ignored for now)
139+
max_iterations: Maximum iterations
140+
position_tolerance: Success threshold in meters
141+
step_size: Step size multiplier (0-1, smaller = more stable)
142+
damping: Damping factor for numerical stability
143+
144+
Returns:
145+
(solution_joints, converged, final_error)
146+
"""
147+
current_joints = initial_joints.copy()
148+
previous_error = float('inf')
149+
150+
for iteration in range(max_iterations):
151+
# Forward kinematics
152+
current_pos, _ = self._forward_kinematics(chain, current_joints)
153+
154+
# Position error
155+
error = target_position - current_pos
156+
error_norm = np.linalg.norm(error)
157+
158+
# Check convergence
159+
if error_norm < position_tolerance:
160+
return current_joints, True, error_norm
161+
162+
# Adaptive step size: increase if improving, decrease if stuck
163+
if error_norm < previous_error:
164+
adaptive_step = min(step_size * 1.2, 1.0) # Increase up to 1.0
165+
else:
166+
adaptive_step = step_size * 0.5 # Decrease if not improving
167+
168+
previous_error = error_norm
169+
170+
# Compute Jacobian
171+
J = self._compute_jacobian(chain, current_joints)
172+
173+
# Damped least squares: delta_q = J^T * (J*J^T + damping*I)^-1 * error
174+
# Simplified: delta_q = J^T * error (Jacobian transpose method)
175+
# More stable: Use pseudo-inverse with damping
176+
JJT = J @ J.T
177+
JJT_damped = JJT + damping * np.eye(3)
178+
179+
try:
180+
delta_q = J.T @ np.linalg.solve(JJT_damped, error)
181+
except np.linalg.LinAlgError:
182+
# Fallback to simple Jacobian transpose
183+
delta_q = J.T @ error
184+
185+
# Update joints with adaptive step size
186+
current_joints = current_joints + adaptive_step * delta_q
187+
188+
# Clamp to joint limits
189+
current_joints = self._clamp_joints(current_joints)
190+
191+
# Optional: Print progress every 10 iterations
192+
if iteration % 10 == 0 and iteration > 0:
193+
print(f" Iteration {iteration}: error = {error_norm*1000:.2f}mm, step = {adaptive_step:.3f}")
194+
195+
# Did not converge
196+
current_pos, _ = self._forward_kinematics(chain, current_joints)
197+
final_error = np.linalg.norm(target_position - current_pos)
198+
199+
return current_joints, False, final_error
200+
201+
def solve_right_arm(
202+
self,
203+
target_position: np.ndarray,
204+
initial_joints: Optional[np.ndarray] = None,
205+
**kwargs
206+
) -> np.ndarray:
207+
"""Solve IK for right arm.
208+
209+
Args:
210+
target_position: Target [x, y, z] position
211+
initial_joints: Initial joint guess (uses zeros if None)
212+
**kwargs: Additional arguments for solve()
213+
214+
Returns:
215+
7 joint angles in radians
216+
"""
217+
if initial_joints is None:
218+
# Default: arms extended sideways (J2=0°)
219+
initial_joints = np.zeros(7)
220+
221+
solution, converged, error = self.solve(
222+
self._right_chain,
223+
target_position,
224+
initial_joints,
225+
**kwargs
226+
)
227+
228+
if not converged:
229+
print(f"Warning: IK did not converge (error: {error*1000:.2f}mm)")
230+
231+
return solution
232+
233+
def solve_left_arm(
234+
self,
235+
target_position: np.ndarray,
236+
initial_joints: Optional[np.ndarray] = None,
237+
**kwargs
238+
) -> np.ndarray:
239+
"""Solve IK for left arm.
240+
241+
Args:
242+
target_position: Target [x, y, z] position
243+
initial_joints: Initial joint guess (uses zeros if None)
244+
**kwargs: Additional arguments for solve()
245+
246+
Returns:
247+
7 joint angles in radians
248+
"""
249+
if initial_joints is None:
250+
# Default: arms extended sideways (J2=0°)
251+
initial_joints = np.zeros(7)
252+
253+
solution, converged, error = self.solve(
254+
self._left_chain,
255+
target_position,
256+
initial_joints,
257+
**kwargs
258+
)
259+
260+
if not converged:
261+
print(f"Warning: IK did not converge (error: {error*1000:.2f}mm)")
262+
263+
return solution
264+
265+
266+
# Convenience wrapper with frame translation
267+
class OpenArmIKWrapper:
268+
"""Wrapper that handles physical<->URDF frame translation."""
269+
270+
def __init__(self):
271+
self.ik_solver = OpenArmJacobianIK()
272+
273+
def physical_to_urdf_right(self, physical_joints_deg: np.ndarray) -> np.ndarray:
274+
"""Convert right arm physical joints to URDF frame (radians)."""
275+
urdf_joints_deg = physical_joints_deg.copy()
276+
urdf_joints_deg[1] -= 90 # J2: Physical 0° → URDF -90°
277+
return np.deg2rad(urdf_joints_deg)
278+
279+
def urdf_to_physical_right(self, urdf_joints_rad: np.ndarray) -> np.ndarray:
280+
"""Convert right arm URDF joints to physical frame (degrees)."""
281+
urdf_joints_deg = np.rad2deg(urdf_joints_rad)
282+
physical_joints_deg = urdf_joints_deg.copy()
283+
physical_joints_deg[1] += 90 # J2: URDF -90° → Physical 0°
284+
return physical_joints_deg
285+
286+
def physical_to_urdf_left(self, physical_joints_deg: np.ndarray) -> np.ndarray:
287+
"""Convert left arm physical joints to URDF frame (radians)."""
288+
urdf_joints_deg = physical_joints_deg.copy()
289+
urdf_joints_deg[1] += 90 # J2: Physical 0° → URDF +90°
290+
return np.deg2rad(urdf_joints_deg)
291+
292+
def urdf_to_physical_left(self, urdf_joints_rad: np.ndarray) -> np.ndarray:
293+
"""Convert left arm URDF joints to physical frame (degrees)."""
294+
urdf_joints_deg = np.rad2deg(urdf_joints_rad)
295+
physical_joints_deg = urdf_joints_deg.copy()
296+
physical_joints_deg[1] -= 90 # J2: URDF +90° → Physical 0°
297+
return physical_joints_deg
298+
299+
def solve_right_arm_physical(
300+
self,
301+
target_position: np.ndarray,
302+
current_physical_joints_deg: np.ndarray,
303+
**kwargs
304+
) -> np.ndarray:
305+
"""Solve IK for right arm, handling frame translation.
306+
307+
Args:
308+
target_position: Target [x, y, z] in meters
309+
current_physical_joints_deg: Current physical joint angles in degrees
310+
311+
Returns:
312+
Physical joint angles in degrees
313+
"""
314+
# Convert to URDF frame
315+
urdf_initial = self.physical_to_urdf_right(current_physical_joints_deg)
316+
317+
# Solve IK in URDF frame
318+
urdf_solution = self.ik_solver.solve_right_arm(
319+
target_position,
320+
initial_joints=urdf_initial,
321+
**kwargs
322+
)
323+
324+
# Convert back to physical frame
325+
physical_solution = self.urdf_to_physical_right(urdf_solution)
326+
327+
return physical_solution
328+
329+
def solve_left_arm_physical(
330+
self,
331+
target_position: np.ndarray,
332+
current_physical_joints_deg: np.ndarray,
333+
**kwargs
334+
) -> np.ndarray:
335+
"""Solve IK for left arm, handling frame translation.
336+
337+
Args:
338+
target_position: Target [x, y, z] in meters
339+
current_physical_joints_deg: Current physical joint angles in degrees
340+
341+
Returns:
342+
Physical joint angles in degrees
343+
"""
344+
# Convert to URDF frame
345+
urdf_initial = self.physical_to_urdf_left(current_physical_joints_deg)
346+
347+
# Solve IK in URDF frame
348+
urdf_solution = self.ik_solver.solve_left_arm(
349+
target_position,
350+
initial_joints=urdf_initial,
351+
**kwargs
352+
)
353+
354+
# Convert back to physical frame
355+
physical_solution = self.urdf_to_physical_left(urdf_solution)
356+
357+
return physical_solution

0 commit comments

Comments
 (0)