实验课2¶
一些信息¶
机器人离线编程与仿真
0167103
解析¶
代码¶
kinematic_invforw2_detail.py¶
# %% Import Libraries (Existing + New)
import numpy as np
import sympy as sp
import math
import matplotlib.pyplot as plt
# mpl_toolkits is no longer needed for Axes3D in recent matplotlib versions
# from mpl_toolkits.mplot3d import Axes3D # For 3D plotting
import time # For animation pause
# Use SymPy's pretty printing
sp.init_printing(use_unicode=True)
# --- Paste your existing functions here ---
# get_robot_params()
# inverse_kinematics(xp, yp, zp, params)
# define_symbolic_fk(params, q1_in, q2_in, q3_in)
# forward_kinematics(q1_in, q2_in, q3_in, params, initial_guess, ...)
# plot_robot(ax, rp, R4, params, title="Robot Configuration")
# --- End of pasted functions ---
# %% 机器人参数 (Robot Parameters) - Copied from your code
def get_robot_params():
"""返回包含机器人尺度参数的字典"""
return {
'a11': 135.0, # 移动平台上连接点到平台中心的距离
'a22': 135.0, # 同上
'a33': 135.0, # 同上
'b11': 570.0, # 固定平台上连接点到平台中心的距离
'b22': 320.0, # 同上
'b33': 320.0, # 同上
'e': 345.0 # 中心柱固定长度
}
# %% 位置逆解函数 (Inverse Kinematics Function) - Copied from your code
def inverse_kinematics(xp, yp, zp, params):
"""
计算机器人逆运动学。
输入:
xp, yp, zp: 末端执行器目标位置 (float)
params: 包含机器人尺寸的字典 (dict)
输出:
一个包含逆解结果的字典,如果无法求解则返回 None:
{
'q1', 'q2', 'q3': 驱动杆计算长度 (float) - 注意其定义可能不标准
'q4': 中心杆可变长度 (float)
'theta1', 'theta2': 平台姿态角 (rad) (float)
'rp': 末端位置向量 (numpy.ndarray, shape (3,1))
'R4': 移动平台旋转矩阵 (numpy.ndarray, shape (3,3))
'w4': 中心杆方向向量 (numpy.ndarray, shape (3,1))
'error': 错误信息 (str or None)
}
"""
a11 = params['a11']
a22 = params['a22']
a33 = params['a33']
b11 = params['b11']
b22 = params['b22']
b33 = params['b33']
e = params['e']
rp = np.array([[xp], [yp], [zp]]) # 列向量
norm_rp = np.linalg.norm(rp)
# Check if target is reachable (outside the fixed central column)
# Add a small tolerance for floating point comparisons
min_dist = e
if norm_rp < min_dist - 1e-9:
return {'error': f"警告:末端位置 (范数 {norm_rp:.2f}) 在中心柱固定长度 (e={e}) 范围内,无法达到。"}
q4 = norm_rp - e
# Ensure q4 (variable length) is not negative
if q4 < 0:
# This case should ideally be caught by the norm_rp check above,
# but as a safeguard:
q4 = 0
# Recalculate norm_rp if we force q4=0, meaning rp is exactly on the sphere of radius e
norm_rp = e
if norm_rp < 1e-9: # Avoid division by zero if rp is at the origin
return {'error': "错误:末端位置在原点,姿态未定义。"}
rp = (rp / np.linalg.norm(rp)) * e # Project onto the sphere surface
# Avoid division by zero when calculating angles
denominator = q4 + e # This is norm_rp
if abs(denominator) < 1e-9:
# This should only happen if rp is the zero vector, handled above.
return {'error': "错误:q4 + e 接近零 (rp 在原点),无法计算角度。"}
# Calculate theta2
# Clip argument to [-1, 1] to handle potential floating point inaccuracies
sin_theta2_arg = xp / denominator
sin_theta2_arg = np.clip(sin_theta2_arg, -1.0, 1.0)
theta2 = np.arcsin(sin_theta2_arg)
# Calculate theta1
cos_theta2 = np.cos(theta2)
if abs(cos_theta2) < 1e-9:
# When theta2 is close to +/- pi/2, the platform is near horizontal.
# Use atan2 for robustness, considering the signs of -yp and zp.
theta1 = np.arctan2(-yp, zp)
else:
# Calculate arguments for atan2 carefully
arg1 = (-yp / denominator) / cos_theta2
arg2 = (zp / denominator) / cos_theta2
# Clip arguments for safety before atan2, although atan2 handles quadrants correctly
# arg1 = np.clip(arg1, -1.0, 1.0) # Sin component
# arg2 = np.clip(arg2, -1.0, 1.0) # Cos component
theta1 = np.arctan2(arg1, arg2)
# Calculate rotation matrix R4
c1, s1 = np.cos(theta1), np.sin(theta1)
c2, s2 = np.cos(theta2), np.sin(theta2)
R4 = np.array([
[c2, 0, s2],
[s1*s2, c1, -s1*c2],
[-c1*s2, s1, c1*c2]
])
# Fixed platform connection points (in fixed frame)
b1 = np.array([[0], [-b11], [0]])
b2 = np.array([[b22], [0], [0]])
b3 = np.array([[-b33], [0], [0]])
# Moving platform connection points (in moving frame {4})
a10 = np.array([[0], [-a11], [0]])
a20 = np.array([[a22], [0], [0]])
a30 = np.array([[-a33], [0], [0]])
# Moving platform connection points relative vector (in fixed frame)
a1 = R4 @ a10
a2 = R4 @ a20
a3 = R4 @ a30
# Central column direction vector w4 (in fixed frame)
# w4 should be rp / norm(rp)
if norm_rp > 1e-9:
w4 = rp / norm_rp
else:
# Handle the case where rp is at the origin (though ideally prevented earlier)
# Default to pointing up Z-axis, though this case is problematic
w4 = np.array([[0], [0], [1.0]])
# Calculate driving rod lengths q1, q2, q3 (using the formula from the original code)
# Note: This definition might differ from the physical length || (rp + a_i) - b_i ||
# The formula seems to represent the length of the vector from B_i to A_i'
# where A_i' is the connection point on the moving platform projected down
# along the central column's variable part.
# L_i = (rp + a_i) - b_i = (e*w4 + q4*w4 + a_i) - b_i
# The formula used is || q4*w4 + a_i - b_i || = || rp - e*w4 + a_i - b_i ||
q1_vec = rp - b1 + a1 - e*w4
q2_vec = rp - b2 + a2 - e*w4
q3_vec = rp - b3 + a3 - e*w4
q1 = np.linalg.norm(q1_vec)
q2 = np.linalg.norm(q2_vec)
q3 = np.linalg.norm(q3_vec)
return {
'q1': q1, 'q2': q2, 'q3': q3,
'q4': q4, 'theta1': theta1, 'theta2': theta2,
'rp': rp, 'R4': R4, 'w4': w4,
'error': None
}
# %% 位置正解 - 符号设置与数值函数生成 (Forward Kinematics - Symbolic Setup) - Copied from your code
def define_symbolic_fk(params, q1_in, q2_in, q3_in):
"""
定义正解的符号方程、雅可比矩阵,并返回数值计算函数。
输入:
params: 机器人参数字典
q1_in, q2_in, q3_in: 输入的驱动杆计算长度
输出:
(Func_sym, J_sym, num_func, num_jac, X_sym)
Func_sym: 符号方程组 (SymPy Matrix)
J_sym: 符号雅可比矩阵 (SymPy Matrix)
num_func: Func_sym 的数值计算函数 (callable)
num_jac: J_sym 的数值计算函数 (callable)
X_sym: 符号变量向量 [q4, theta1, theta2] (SymPy Matrix)
"""
a11 = params['a11']
a22 = params['a22']
a33 = params['a33']
b11 = params['b11']
b22 = params['b22']
b33 = params['b33']
# e = params['e'] # e 不直接出现在 F, G, H 方程中
# 定义符号变量
q4_sym, theta1_sym, theta2_sym = sp.symbols('q4_sym theta1_sym theta2_sym')
X_sym = sp.Matrix([q4_sym, theta1_sym, theta2_sym])
# 定义约束方程 F, G, H (来自连杆闭环)
# 这些方程将输入的 q1_in, q2_in, q3_in 与未知的 q4, theta1, theta2 联系起来
# Based on the inverse kinematics formula q_i = || q4*w4 + a_i - b_i ||
# where w4 = [s2; -s1*c2; c1*c2]
# a1 = R4*a10 = R4*[0; -a11; 0] = [-a11*0; a11*c1; a11*s1] ? No, R4 is complex.
# Let's use the formula provided directly in the MATLAB symbolic part if possible,
# assuming it corresponds to the || q4*w4 + a_i - b_i ||^2 = q_i^2 relation.
# R4 = [[c2, 0, s2], [s1*s2, c1, -s1*c2], [-c1*s2, s1, c1*c2]]
# a10 = [0; -a11; 0] -> a1 = R4*a10 = [0; -a11*c1; -a11*s1]
# a20 = [a22; 0; 0] -> a2 = R4*a20 = [a22*c2; a22*s1*s2; -a22*c1*s2]
# a30 = [-a33; 0; 0] -> a3 = R4*a30 = [-a33*c2; -a33*s1*s2; a33*c1*s2]
# w4 = [s2; -s1*c2; c1*c2]
# b1 = [0; -b11; 0]
# b2 = [b22; 0; 0]
# b3 = [-b33; 0; 0]
# Vector L1 = q4*w4 + a1 - b1
# L1 = [q4*s2 + 0 - 0;
# q4*(-s1*c2) - a11*c1 - (-b11);
# q4*(c1*c2) - a11*s1 - 0]
# L1 = [q4*s2; -q4*s1*c2 - a11*c1 + b11; q4*c1*c2 - a11*s1]
# ||L1||^2 = (q4*s2)^2 + (-q4*s1*c2 - a11*c1 + b11)^2 + (q4*c1*c2 - a11*s1)^2 = q1^2
# Vector L2 = q4*w4 + a2 - b2
# L2 = [q4*s2 + a22*c2 - b22;
# q4*(-s1*c2) + a22*s1*s2 - 0;
# q4*(c1*c2) - a22*c1*s2 - 0]
# L2 = [q4*s2 + a22*c2 - b22; -q4*s1*c2 + a22*s1*s2; q4*c1*c2 - a22*c1*s2]
# ||L2||^2 = (q4*s2 + a22*c2 - b22)^2 + (-q4*s1*c2 + a22*s1*s2)^2 + (q4*c1*c2 - a22*c1*s2)^2 = q2^2
# Vector L3 = q4*w4 + a3 - b3
# L3 = [q4*s2 - a33*c2 - (-b33);
# q4*(-s1*c2) - a33*s1*s2 - 0;
# q4*(c1*c2) + a33*c1*s2 - 0]
# L3 = [q4*s2 - a33*c2 + b33; -q4*s1*c2 - a33*s1*s2; q4*c1*c2 + a33*c1*s2]
# ||L3||^2 = (q4*s2 - a33*c2 + b33)^2 + (-q4*s1*c2 - a33*s1*s2)^2 + (q4*c1*c2 + a33*c1*s2)^2 = q3^2
# Let's check the provided F_sym, G_sym, H_sym if they match the expansion of ||Li||^2 - qi^2 = 0
# F_sym = a11**2 + b11**2 + q4_sym**2 - q1_in**2 \
# - 2*a11*b11*sp.cos(theta1_sym) \
# - 2*b11*q4_sym*sp.sin(theta1_sym)*sp.cos(theta2_sym)
# G_sym = a22**2 + b22**2 + q4_sym**2 - q2_in**2 \
# - 2*b22*q4_sym*sp.sin(theta2_sym) \
# - 2*a22*b22*sp.cos(theta2_sym)
# H_sym = a33**2 + b33**2 + q4_sym**2 - q3_in**2 \
# + 2*b33*q4_sym*sp.sin(theta2_sym) \
# - 2*a33*b33*sp.cos(theta2_sym)
# These symbolic equations seem simpler than the full expansion of ||Li||^2.
# They might be derived from a different geometric constraint or simplification.
# IMPORTANT: We will use the F, G, H equations provided in the original code,
# assuming they are correct for the specific robot mechanism intended.
c1_sym, s1_sym = sp.cos(theta1_sym), sp.sin(theta1_sym)
c2_sym, s2_sym = sp.cos(theta2_sym), sp.sin(theta2_sym)
F_sym = a11**2 + b11**2 + q4_sym**2 - q1_in**2 \
- 2*a11*b11*c1_sym \
- 2*b11*q4_sym*s1_sym*c2_sym # Check signs: w4_y = -s1*c2. Term is -2*b1_y*(q4*w4_y)? No.
# Let's re-verify the terms in F_sym based on ||L1||^2
# ||L1||^2 = (q4*s2)**2 + (-q4*s1*c2 - a11*c1 + b11)**2 + (q4*c1*c2 - a11*s1)**2
# = q4^2*s2^2
# + (q4*s1*c2 + a11*c1 - b11)^2 + (q4*c1*c2 - a11*s1)^2
# + q4^2*s1^2*c2^2 + a11^2*c1^2 + b11^2 + 2*q4*s1*c2*a11*c1 - 2*q4*s1*c2*b11 - 2*a11*c1*b11
# + q4^2*c1^2*c2^2 - 2*q4*c1*c2*a11*s1 + a11^2*s1^2
# Group terms:
# q4^2 terms: q4^2*s2^2 + q4^2*s1^2*c2^2 + q4^2*c1^2*c2^2 = q4^2*s2^2 + q4^2*c2^2*(s1^2+c1^2) = q4^2*s2^2 + q4^2*c2^2 = q4^2*(s2^2+c2^2) = q4^2
# a11^2 terms: a11^2*c1^2 + a11^2*s1^2 = a11^2*(c1^2+s1^2) = a11^2
# b11^2 terms: b11^2
# q4*a11 terms: 2*q4*s1*c2*a11*c1 - 2*q4*c1*c2*a11*s1 = 0
# q4*b11 terms: -2*q4*s1*c2*b11
# a11*b11 terms: -2*a11*c1*b11
# So, ||L1||^2 = q4^2 + a11^2 + b11^2 - 2*q4*s1*c2*b11 - 2*a11*c1*b11
# Equation F = ||L1||^2 - q1^2 = 0 becomes:
# q4^2 + a11^2 + b11^2 - 2*b11*q4*s1*c2 - 2*a11*b11*c1 - q1^2 = 0
# Comparing with the provided F_sym:
# F_sym = a11**2 + b11**2 + q4_sym**2 - q1_in**2 - 2*a11*b11*c1_sym - 2*b11*q4_sym*s1_sym*c2_sym
# They MATCH! Good.
# Now check G_sym based on ||L2||^2
# L2 = [q4*s2 + a22*c2 - b22; -q4*s1*c2 + a22*s1*s2; q4*c1*c2 - a22*c1*s2]
# ||L2||^2 = (q4*s2 + a22*c2 - b22)^2 + (-q4*s1*c2 + a22*s1*s2)^2 + (q4*c1*c2 - a22*c1*s2)^2
# = (q4*s2 + a22*c2 - b22)^2 + s1^2*(-q4*c2 + a22*s2)^2 + c1^2*(q4*c2 - a22*s2)^2
# = (q4*s2 + a22*c2 - b22)^2 + (s1^2+c1^2)*(q4*c2 - a22*s2)^2
# = (q4*s2 + a22*c2 - b22)^2 + (q4*c2 - a22*s2)^2
# Expand:
# (q4*s2)^2 + (a22*c2)^2 + b22^2 + 2*q4*s2*a22*c2 - 2*q4*s2*b22 - 2*a22*c2*b22
# + (q4*c2)^2 - 2*q4*c2*a22*s2 + (a22*s2)^2
# Group terms:
# q4^2 terms: q4^2*s2^2 + q4^2*c2^2 = q4^2
# a22^2 terms: a22^2*c2^2 + a22^2*s2^2 = a22^2
# b22^2 terms: b22^2
# q4*a22 terms: 2*q4*s2*a22*c2 - 2*q4*c2*a22*s2 = 0
# q4*b22 terms: -2*q4*s2*b22
# a22*b22 terms: -2*a22*c2*b22
# So, ||L2||^2 = q4^2 + a22^2 + b22^2 - 2*q4*s2*b22 - 2*a22*c2*b22
# Equation G = ||L2||^2 - q2^2 = 0 becomes:
# q4^2 + a22^2 + b22^2 - 2*b22*q4*s2 - 2*a22*b22*c2 - q2^2 = 0
# Comparing with provided G_sym:
# G_sym = a22**2 + b22**2 + q4_sym**2 - q2_in**2 - 2*b22*q4_sym*s2_sym - 2*a22*b22*c2_sym
# They MATCH! Good.
# Now check H_sym based on ||L3||^2
# L3 = [q4*s2 - a33*c2 + b33; -q4*s1*c2 - a33*s1*s2; q4*c1*c2 + a33*c1*s2]
# ||L3||^2 = (q4*s2 - a33*c2 + b33)^2 + (-q4*s1*c2 - a33*s1*s2)^2 + (q4*c1*c2 + a33*c1*s2)^2
# = (q4*s2 - a33*c2 + b33)^2 + s1^2*(-q4*c2 - a33*s2)^2 + c1^2*(q4*c2 + a33*s2)^2
# = (q4*s2 - a33*c2 + b33)^2 + (s1^2+c1^2)*(q4*c2 + a33*s2)^2
# = (q4*s2 - a33*c2 + b33)^2 + (q4*c2 + a33*s2)^2
# Expand:
# (q4*s2)^2 + (a33*c2)^2 + b33^2 - 2*q4*s2*a33*c2 + 2*q4*s2*b33 - 2*a33*c2*b33
# + (q4*c2)^2 + 2*q4*c2*a33*s2 + (a33*s2)^2
# Group terms:
# q4^2 terms: q4^2*s2^2 + q4^2*c2^2 = q4^2
# a33^2 terms: a33^2*c2^2 + a33^2*s2^2 = a33^2
# b33^2 terms: b33^2
# q4*a33 terms: -2*q4*s2*a33*c2 + 2*q4*c2*a33*s2 = 0
# q4*b33 terms: 2*q4*s2*b33
# a33*b33 terms: -2*a33*c2*b33
# So, ||L3||^2 = q4^2 + a33^2 + b33^2 + 2*q4*s2*b33 - 2*a33*c2*b33
# Equation H = ||L3||^2 - q3^2 = 0 becomes:
# q4^2 + a33^2 + b33^2 + 2*b33*q4*s2 - 2*a33*b33*c2 - q3^2 = 0
# Comparing with provided H_sym:
# H_sym = a33**2 + b33**2 + q4_sym**2 - q3_in**2 + 2*b33*q4_sym*s2_sym - 2*a33*b33*c2_sym
# They MATCH! Excellent.
# The symbolic equations provided are correct based on the IK definition q_i = || rp - e*w4 + a_i - b_i ||
F_sym = a11**2 + b11**2 + q4_sym**2 - q1_in**2 \
- 2*a11*b11*c1_sym \
- 2*b11*q4_sym*s1_sym*c2_sym
G_sym = a22**2 + b22**2 + q4_sym**2 - q2_in**2 \
- 2*b22*q4_sym*s2_sym \
- 2*a22*b22*c2_sym
H_sym = a33**2 + b33**2 + q4_sym**2 - q3_in**2 \
+ 2*b33*q4_sym*s2_sym \
- 2*a33*b33*c2_sym
Func_sym = sp.Matrix([F_sym, G_sym, H_sym])
# 计算雅可比矩阵 J = d(Func)/d(X)
J_sym = Func_sym.jacobian(X_sym)
# 转换为快速数值函数
num_func = sp.lambdify((q4_sym, theta1_sym, theta2_sym), Func_sym, 'numpy')
num_jac = sp.lambdify((q4_sym, theta1_sym, theta2_sym), J_sym, 'numpy')
return Func_sym, J_sym, num_func, num_jac, X_sym
# %% 位置正解 - 牛顿迭代求解器 (Forward Kinematics - Newton Solver) - Copied from your code
def forward_kinematics(q1_in, q2_in, q3_in, params, initial_guess, max_iter=100, tolerance=1e-6):
"""
使用牛顿法迭代求解正运动学。
输入:
q1_in, q2_in, q3_in: 驱动杆计算长度 (float)
params: 机器人参数字典 (dict)
initial_guess: [q4_guess, theta1_guess, theta2_guess] (list or tuple)
max_iter: 最大迭代次数 (int)
tolerance: 收敛阈值 (float)
输出:
一个包含正解结果的字典,如果失败则包含错误信息:
{
'xp_fk', 'yp_fk', 'zp_fk': 计算得到的末端位置 (float)
'q4_k', 'theta1_k', 'theta2_k': 计算得到的姿态变量 (float)
'iterations': 实际迭代次数 (int)
'final_error_norm': 最终方程误差范数 (float)
'converged': 是否收敛 (bool)
'error': 错误信息 (str or None)
}
"""
e = params['e']
# 获取符号定义和数值函数
# Note: Defining symbolic functions every call can be slow.
# Consider defining them once outside if performance is critical.
Func_sym, J_sym, num_func, num_jac, X_sym = define_symbolic_fk(params, q1_in, q2_in, q3_in)
# 初始猜测值
# Ensure initial_guess is a mutable numpy array for in-place updates
X_k = np.array(initial_guess, dtype=float)
iterations = 0
converged = False
final_error_norm = float('inf')
error_message = None
for i in range(max_iter):
iterations = i
q4_k, theta1_k, theta2_k = X_k[0], X_k[1], X_k[2]
# Ensure angles stay within reasonable bounds if needed, e.g., -pi to pi for theta1
# X_k[1] = (X_k[1] + np.pi) % (2 * np.pi) - np.pi
# However, Newton's method might jump outside, rely on trig functions in F, J
# 计算当前点的函数值 F(X_k) (需要是列向量 for numpy solve, but lambdify gives matrix)
try:
F_k_mat = num_func(q4_k, theta1_k, theta2_k)
# Ensure F_k is a flat array (vector)
F_k = np.array(F_k_mat).flatten()
if F_k.shape != (3,):
raise ValueError(f"Function output shape mismatch: {F_k.shape}")
except (TypeError, ValueError, NameError) as e_eval:
error_message = f"Error evaluating F at iteration {i}: {e_eval}. Check inputs/lambdify."
# print(f"\n{error_message}")
# print(f"X_k = {X_k}")
break # Exit loop on evaluation error
# 检查收敛性
current_error_norm = np.linalg.norm(F_k)
if current_error_norm < tolerance:
converged = True
final_error_norm = current_error_norm
break
# 计算当前点的雅可比矩阵 J(X_k)
try:
J_k = num_jac(q4_k, theta1_k, theta2_k)
# Ensure J_k is a 2D numpy array
J_k = np.array(J_k).astype(float)
if J_k.shape != (3, 3):
raise ValueError(f"Jacobian output shape mismatch: {J_k.shape}")
except (TypeError, ValueError, NameError) as e_eval:
error_message = f"Error evaluating J at iteration {i}: {e_eval}. Check inputs/lambdify."
# print(f"\n{error_message}")
# print(f"X_k = {X_k}")
break # Exit loop on evaluation error
# 求解线性方程组 J(X_k) * delta_X = -F(X_k)
try:
# Use pseudo-inverse for potentially singular Jacobian (more robust but slower)
# delta_X = np.linalg.pinv(J_k) @ (-F_k)
# Or use standard solve and catch singularity
delta_X = np.linalg.solve(J_k, -F_k)
# Add damping factor if steps are too large (optional)
# step_norm = np.linalg.norm(delta_X)
# max_step = 1.0 # Adjust as needed
# if step_norm > max_step:
# delta_X = delta_X * (max_step / step_norm)
except np.linalg.LinAlgError:
error_message = f"Jacobian matrix singular at iteration {i}. Cannot solve."
# print(f"\n{error_message}")
# print(f"J_k = {J_k}")
final_error_norm = current_error_norm # Record error before failing
converged = False
break # Exit loop if singular
# 更新变量 X_{k+1} = X_k + delta_X
X_k += delta_X
# Check for NaN or Inf values
if not np.all(np.isfinite(X_k)):
error_message = f"Non-finite values encountered at iteration {i}. Divergence?"
# print(f"\n{error_message}")
converged = False
break
# After loop finishes (break or max_iter)
if converged:
error_message = None
iterations += 1 # Increment iterations to count the last successful one
elif error_message is None: # Reached max_iter without converging or explicit error
error_message = "Reached maximum iterations without converging."
final_error_norm = np.linalg.norm(num_func(X_k[0], X_k[1], X_k[2]).flatten())
# Calculate final end effector position if converged
xp_fk, yp_fk, zp_fk = None, None, None
if converged:
q4_final, theta1_final, theta2_final = X_k
# Use the forward kinematics equations: rp = (e + q4) * w4
norm_rp_final = e + q4_final
c1_f, s1_f = np.cos(theta1_final), np.sin(theta1_final)
c2_f, s2_f = np.cos(theta2_final), np.sin(theta2_final)
w4_final = np.array([s2_f, -s1_f*c2_f, c1_f*c2_f])
rp_final = norm_rp_final * w4_final
xp_fk, yp_fk, zp_fk = rp_final[0], rp_final[1], rp_final[2]
return {
'xp_fk': xp_fk, 'yp_fk': yp_fk, 'zp_fk': zp_fk,
'q4_k': X_k[0], 'theta1_k': X_k[1], 'theta2_k': X_k[2],
'iterations': iterations, 'final_error_norm': final_error_norm,
'converged': converged, 'error': error_message
}
# %% 可视化函数 (Visualization Function) - Copied from your code
def plot_robot(ax, rp, R4, params, title="Robot Configuration"):
"""
在给定的 3D 坐标轴上绘制机器人结构。
输入:
ax: Matplotlib 3D 坐标轴对象 (Axes3D)
rp: 末端位置向量 (numpy.ndarray, shape (3,1) or (3,))
R4: 移动平台旋转矩阵 (numpy.ndarray, shape (3,3))
params: 机器人参数字典 (dict)
title: 图表标题 (str)
"""
a11 = params['a11']
a22 = params['a22']
a33 = params['a33']
b11 = params['b11']
b22 = params['b22']
b33 = params['b33']
e = params['e']
# Ensure rp is a flat array for easier indexing
P = np.array(rp).flatten()
if P.shape != (3,):
print(f"Warning: Unexpected rp shape in plot_robot: {rp.shape}")
# Attempt to reshape if it's (3,1)
if P.shape == (3, 1):
P = P.flatten()
else:
# Cannot plot if shape is wrong
ax.set_title(f"{title}\n(Error: Invalid rp input)")
return
# --- 计算关键点坐标 ---
# 原点 O
O = np.array([0, 0, 0])
# 固定平台连接点 B1, B2, B3 (在固定坐标系)
B1 = np.array([0, -b11, 0])
B2 = np.array([b22, 0, 0])
B3 = np.array([-b33, 0, 0])
# 移动平台连接点 A10, A20, A30 (在移动平台自身坐标系 {4})
A10 = np.array([0, -a11, 0])
A20 = np.array([a22, 0, 0])
A30 = np.array([-a33, 0, 0])
# 移动平台连接点 A1, A2, A3 (相对平台中心 rp 的向量,在固定坐标系)
# Ensure R4 is a 3x3 matrix
if R4.shape != (3, 3):
print(f"Warning: Unexpected R4 shape in plot_robot: {R4.shape}")
ax.set_title(f"{title}\n(Error: Invalid R4 input)")
return
A1_vec = R4 @ A10.reshape(3, 1)
A2_vec = R4 @ A20.reshape(3, 1)
A3_vec = R4 @ A30.reshape(3, 1)
# 移动平台连接点 A1', A2', A3' 的绝对坐标 (在固定坐标系)
A1_prime = P + A1_vec.flatten()
A2_prime = P + A2_vec.flatten()
A3_prime = P + A3_vec.flatten()
# 中心柱顶端 E (固定坐标系)
norm_P = np.linalg.norm(P)
if norm_P > 1e-9:
w4_vis = P / norm_P
else: # Handle case where P is at the origin
w4_vis = np.array([0, 0, 1.0]) # Default to Z-axis
E_pt = e * w4_vis # Coordinates of the top of the fixed part
# --- 清除之前的绘图 ---
ax.cla()
# --- 绘制结构 ---
# 固定平台 (灰色虚线)
fixed_platform_pts = np.array([B1, B2, B3, B1])
ax.plot(fixed_platform_pts[:, 0], fixed_platform_pts[:, 1], fixed_platform_pts[:, 2], 'k--', lw=1, color='gray', label='_Fixed Platform Base') # Underscore hides from legend unless specified
ax.scatter(fixed_platform_pts[:-1, 0], fixed_platform_pts[:-1, 1], fixed_platform_pts[:-1, 2], c='black', marker='s', s=40, label='Base Joints (B1-B3)')
# 移动平台 (蓝色实线)
moving_platform_pts = np.array([A1_prime, A2_prime, A3_prime, A1_prime])
ax.plot(moving_platform_pts[:, 0], moving_platform_pts[:, 1], moving_platform_pts[:, 2], 'b-', lw=2, label='Moving Platform')
ax.scatter(moving_platform_pts[:-1, 0], moving_platform_pts[:-1, 1], moving_platform_pts[:-1, 2], c='blue', marker='o', s=40, label='Platform Joints (A1\'-A3\')')
# 驱动杆 (红色实线) - 绘制物理连杆 B_i 到 A'_i
# These are the physical links whose lengths might NOT be q1, q2, q3 from IK
ax.plot([B1[0], A1_prime[0]], [B1[1], A1_prime[1]], [B1[2], A1_prime[2]], 'r-', lw=1.5, label='Driving Rods (Physical)')
ax.plot([B2[0], A2_prime[0]], [B2[1], A2_prime[1]], [B2[2], A2_prime[2]], 'r-', lw=1.5)
ax.plot([B3[0], A3_prime[0]], [B3[1], A3_prime[1]], [B3[2], A3_prime[2]], 'r-', lw=1.5)
# 中心柱 (绿色)
ax.plot([O[0], E_pt[0]], [O[1], E_pt[1]], [O[2], E_pt[2]], 'g-', linewidth=4, label='Central Column (Fixed Part)') # Fixed part
ax.plot([E_pt[0], P[0]], [E_pt[1], P[1]], [E_pt[2], P[2]], 'g--', linewidth=2, label='Central Column (Variable Part)') # Variable part (length q4)
# 标出末端点 P
ax.scatter(P[0], P[1], P[2], c='magenta', marker='*', s=150, label='End Effector (P)', depthshade=False, zorder=10)
# 标出原点 O
ax.scatter(O[0], O[1], O[2], c='black', marker='x', s=50, label='Origin (O)')
# --- 设置绘图属性 ---
ax.set_xlabel('X (mm)')
ax.set_ylabel('Y (mm)')
ax.set_zlabel('Z (mm)')
ax.set_title(title)
# 设置大致的坐标轴范围,可以根据需要调整
# Find max extent dynamically
all_points = np.vstack([O, B1, B2, B3, P, A1_prime, A2_prime, A3_prime, E_pt])
max_vals = np.max(all_points, axis=0)
min_vals = np.min(all_points, axis=0)
center = (max_vals + min_vals) / 2
max_range = np.max(max_vals - min_vals)
if max_range < 100: max_range = max(b11, b22, b33, e) # Ensure minimum range if points are close
plot_radius = max_range * 0.6 # Add some padding
ax.set_xlim(center[0] - plot_radius, center[0] + plot_radius)
ax.set_ylim(center[1] - plot_radius, center[1] + plot_radius)
# Ensure Z starts from 0 or slightly below
z_min = min(0, min_vals[2]) - 50
z_max = max(e + 500, max_vals[2]) + 50 # Ensure Z range is sufficient
ax.set_zlim(z_min, z_max)
# Attempt to set aspect ratio to 'equal' for a more realistic view
try:
# Use 'equal' for axes box, not data limits necessarily
ax.set_aspect('equal', adjustable='box')
# print("Applied 'equal' aspect ratio.")
except NotImplementedError:
# Fallback: Manually set limits to be cubic around the center
# This was the logic previously used, keep it as fallback
x_limits = ax.get_xlim()
y_limits = ax.get_ylim()
z_limits = ax.get_zlim()
x_range = abs(x_limits[1] - x_limits[0])
y_range = abs(y_limits[1] - y_limits[0])
z_range = abs(z_limits[1] - z_limits[0])
plot_radius_manual = 0.5 * max([x_range, y_range, z_range])
mid_x = 0.5 * (x_limits[0] + x_limits[1])
mid_y = 0.5 * (y_limits[0] + y_limits[1])
mid_z = 0.5 * (z_limits[0] + z_limits[1])
ax.set_xlim(mid_x - plot_radius_manual, mid_x + plot_radius_manual)
ax.set_ylim(mid_y - plot_radius_manual, mid_y + plot_radius_manual)
ax.set_zlim(mid_z - plot_radius_manual, mid_z + plot_radius_manual)
print("Warning: 3D equal aspect ratio not fully supported. Attempted manual scaling.")
ax.legend(fontsize='small')
ax.grid(True)
# %% S-Curve Velocity Profile Generation (Replicating MATLAB's Svelocity_motion) - CORRECTED
def s_velocity_motion(vmax, TA, sall, ts):
"""
Generates an S-curve (trapezoidal acceleration) velocity profile.
This function aims to replicate the specific MATLAB implementation provided,
including its calculation of intermediate times and jerk.
Note: The calculation of T1, T2, T4, T5 and je seems simplified/non-standard
compared to typical 7-segment S-curve planning, but the piecewise
formulas *are* used. We replicate the MATLAB code's behavior.
Inputs:
vmax: Maximum velocity (mm/s)
TA: Total time for acceleration phase (s) - Used to derive amax and je
sall: Total distance to travel (mm)
ts: Time step (s)
Outputs:
ac: numpy array of acceleration values (mm/s^2)
vc: numpy array of velocity values (mm/s)
sc: numpy array of displacement values (mm)
time_vec: numpy array of time points (s)
params: Dictionary containing intermediate calculation parameters (T1-T5, je, etc.)
"""
if vmax <= 0 or TA <= 0 or sall <= 0 or ts <= 0:
raise ValueError("vmax, TA, sall, and ts must be positive.")
# --- Calculations as per MATLAB code ---
amax = vmax / TA # Max acceleration derived from vmax and TA
pi_val = np.pi # Use numpy's pi
# These T calculations seem specific to the MATLAB code's logic
T2 = vmax / amax # This just results in T2 = TA
T1 = T2 # T1 = TA
T4 = T2 # T4 = TA (Deceleration time)
T5 = T1 # T5 = TA (Deceleration jerk time?)
# Jerk calculation based on the derived amax and T1 (=TA)
je = amax / T1
# Calculate constant velocity time T3
# **Reverting to exact MATLAB calculation replication:**
# Assume the MATLAB calculation for T3 is intended, despite looking odd.
# The formula used in the MATLAB snippet appears to be: T3 = (sall - 2*je*T1^3)/vmax;
# Let's re-evaluate if this makes sense or if the total time formula t3=t2+T3 should be used differently.
# The piecewise formulas depend on T1, T2, T3, T4, T5.
# Let's stick to the MATLAB formula for T3 for direct replication:
T3 = (sall - 2 * je * T1**3) / vmax # <<< THIS LINE IS NOW UNCOMMENTED
# Check if T3 is negative, meaning vmax is not reached (triangular profile needed)
if T3 < 0:
# This profile generation assumes vmax is reached.
# A more robust implementation would handle the triangular case.
print(f"Warning: Calculated T3 ({T3:.3f}) is negative. vmax may not be reached for given sall, vmax, TA.")
print("Proceeding with T3=0 (assuming minimal constant velocity time). Results might be inaccurate.")
# Adjust parameters for a triangular profile (approximate here)
# Need to recalculate TA or vmax based on sall.
# Or, simply set T3 = 0 and let the profile run.
# If T3 was negative, reset it to 0 for the rest of the calculations.
T3 = 0
# Note: The total time calculation below will now use T3=0
# Time boundaries based on MATLAB logic (T1=T2=T4=T5=TA)
t1 = T1
t2 = T1 + T2
t3 = T1 + T2 + T3 # Now T3 has a value (either calculated or 0)
t4 = T1 + T2 + T3 + T4
t5 = T1 + T2 + T3 + T4 + T5 # Total time
# Store parameters
params = {'vmax': vmax, 'TA': TA, 'sall': sall, 'ts': ts,
'amax': amax, 'je': je, 'T1': T1, 'T2': T2, 'T3': T3, 'T4': T4, 'T5': T5,
't1': t1, 't2': t2, 't3': t3, 't4': t4, 't5': t5}
# --- Generate Time Vector ---
# Ensure t5 is not negative (could happen if T3 was extremely negative before clamping)
if t5 < 0: t5 = 0
nt = int(np.ceil(t5 / ts)) + 1 # Number of points including t=0 and t=t5
if nt <= 1: # Handle edge case of zero duration
nt = 2 # Need at least start and end point
time_vec = np.array([0.0, t5])
actual_ts = t5
else:
time_vec = np.linspace(0, t5, nt)
actual_ts = t5 / (nt - 1)
params['actual_ts'] = actual_ts
params['nt'] = nt
# --- Initialize Output Arrays ---
ac = np.zeros(nt)
vc = np.zeros(nt)
sc = np.zeros(nt)
# --- Loop through time and apply piecewise formulas ---
# Use the formulas exactly as in the MATLAB snippet images 5 & 6
for i, t in enumerate(time_vec):
# Note: Using <= for boundaries to match typical closed intervals [t_start, t_end]
# The MATLAB code uses combinations of <= and <. We'll try to match it.
# Pre-calculate boundary values to avoid redundant calculations inside loops
# Values at t1
vc_t1 = 0.5 * je * T1**2
sc_t1 = (1/6) * je * T1**3
# Values at t2 (end of segment 2)
# Duration of segment 2 is T2
vc_t2 = vc_t1 + amax * T2
sc_t2_matlab = sc_t1 + 0.5 * je * T1**2 * T2 + 0.5 * amax * T2**2 # Using MATLAB sc formula
# Values at t3 (end of segment 3)
# Duration of segment 3 is T3
vc_t3 = vmax # Assumes constant velocity phase uses vmax
sc_t3 = sc_t2_matlab + vmax * T3
# Values at t4 (end of segment 4)
# Duration of segment 4 is T4
vc_t4 = vmax - 0.5 * je * T4**2
sc_t4 = sc_t3 + vmax * T4 - (1/6) * je * T4**3
# Values at t5 (end of segment 5)
# Duration of segment 5 is T5
vc_t5 = vc_t4 - amax * T5
sc_t5 = sc_t4 + vc_t4 * T5 - 0.5 * amax * T5**2
# MATLAB: if(t>=0 && t<=t1)
if 0 <= t <= t1 + 1e-9: # Add tolerance for float comparison
ac[i] = je * t
vc[i] = 0.5 * je * t**2
sc[i] = (1/6) * je * t**3
# MATLAB: else if (t>t1 && t<=t2)
elif t1 < t <= t2 + 1e-9:
ac[i] = amax
vc[i] = vc_t1 + amax * (t - t1)
sc[i] = sc_t1 + 0.5 * je * T1**2 * (t - t1) + 0.5 * amax * (t - t1)**2
# MATLAB: else if (t>t2 && t<=t3)
elif t2 < t <= t3 + 1e-9:
ac[i] = 0
vc[i] = vmax # Use vmax as specified in the MATLAB formula
sc[i] = sc_t2_matlab + vmax * (t - t2)
# MATLAB: else if (t>t3 && t<=t4)
elif t3 < t <= t4 + 1e-9:
ac[i] = -je * (t - t3)
vc[i] = vmax - 0.5 * je * (t - t3)**2
sc[i] = sc_t3 + vmax * (t - t3) - (1/6) * je * (t - t3)**3
# MATLAB: else if (t>t4 && t<=t5)
# Handle the last segment carefully to include t=t5
elif t4 < t <= t5 + 1e-9:
# Check if it's the very last point
is_last_point = (i == nt - 1)
ac[i] = -amax
# Ensure velocity doesn't overshoot zero if profile is complete
calculated_vc = vc_t4 - amax * (t - t4)
# Since the MATLAB profile might be incomplete, we replicate its behavior
vc[i] = calculated_vc
sc[i] = sc_t4 + vc_t4 * (t - t4) - 0.5 * amax * (t - t4)**2
# If it's the last point, explicitly set to final calculated values
if is_last_point:
vc[i] = vc_t5
sc[i] = sc_t5
elif t > t5 + 1e-9: # Should not happen with linspace, but as safeguard
print(f"Warning: Time t={t} exceeded t5={t5}. Using last values.")
if i > 0:
ac[i], vc[i], sc[i] = ac[i-1], vc[i-1], sc[i-1]
else: # If t5 is near zero and first point is already > t5
ac[i], vc[i], sc[i] = 0, 0, 0
# Final checks:
if nt > 0:
# Report final calculated state
print(f"Profile end state: t={time_vec[-1]:.4f}, s={sc[-1]:.4f}, v={vc[-1]:.4f}, a={ac[-1]:.4f}")
# Check final displacement
if abs(sc[-1] - sall) > max(1e-3, sall * 0.01): # Tolerance relative or absolute
print(f"Warning: Final displacement sc[-1] ({sc[-1]:.4f}) differs significantly from target sall ({sall:.4f}). Check TA, vmax parameters.")
# print(f"Profile parameters: {params}")
# Check final velocity (expect non-zero based on MATLAB profile)
# if abs(vc[-1]) > 1e-3: # Tolerance for zero velocity
# print(f"Note: Final calculated velocity vc[-1] = {vc[-1]:.4f} (Profile might be incomplete based on MATLAB code)")
return ac, vc, sc, time_vec, params
# --- Rest of the script remains the same ---
# (Paste your kinematics functions here)
# ...
# if __name__ == "__main__":
# ... (parameter setup, calling s_velocity_motion, IK loop, plotting, animation) ...
# ...
# %% --- Main Trajectory Planning and Execution ---
if __name__ == "__main__":
# --- Get Robot Parameters ---
robot_params = get_robot_params()
print("--- Robot Parameters ---")
for key, value in robot_params.items():
print(f"{key}: {value}")
# --- Trajectory Parameters (from MATLAB snippet) ---
print("\n--- Trajectory Parameters ---")
r = 200.0 # Radius (mm)
vmax = 1000.0 # Max velocity (mm/s) (60000/60)
TA = 0.2 # Acceleration phase time (s) (200 * 0.001)
ts = 0.01 # Time step (s) (10 * 0.001)
pi = np.pi # Use numpy pi
# Circular path parameters
sall = 2 * pi * r # Total path length (circumference)
z_level = 1200.0 # Constant Z height for the circle
y_offset = -190.0 # Center of circle offset in Y
print(f"Radius (r): {r:.1f} mm")
print(f"Max Velocity (vmax): {vmax:.1f} mm/s")
print(f"Acceleration Time (TA): {TA:.3f} s")
print(f"Time Step (ts): {ts:.3f} s")
print(f"Total Path Length (sall): {sall:.3f} mm")
print(f"Z Level: {z_level:.1f} mm")
print(f"Y Offset: {y_offset:.1f} mm")
print(f"Circle Center: (0, {y_offset:.1f}, {z_level:.1f})")
# --- Generate Motion Profile ---
print("\n--- Generating S-Curve Motion Profile ---")
try:
ac, vc, sc, time_vec, profile_params = s_velocity_motion(vmax, TA, sall, ts)
nt = len(time_vec)
print(f"Generated {nt} points.")
print(f"Calculated total time (t5): {profile_params['t5']:.3f} s")
print(f"Actual time step: {profile_params['actual_ts']:.5f} s")
# Plot the motion profile
fig_prof, axs_prof = plt.subplots(3, 1, sharex=True, figsize=(10, 8))
axs_prof[0].plot(time_vec, sc, label='Displacement (s)')
axs_prof[0].set_ylabel('s (mm)')
axs_prof[0].grid(True)
axs_prof[0].legend()
axs_prof[0].set_title('S-Curve Motion Profile (Replicating MATLAB)')
axs_prof[1].plot(time_vec, vc, label='Velocity (v)')
axs_prof[1].set_ylabel('v (mm/s)')
axs_prof[1].grid(True)
axs_prof[1].legend()
axs_prof[2].plot(time_vec, ac, label='Acceleration (a)')
axs_prof[2].set_ylabel('a (mm/s^2)')
axs_prof[2].set_xlabel('Time (s)')
axs_prof[2].grid(True)
axs_prof[2].legend()
fig_prof.tight_layout()
plt.show(block=False)
except ValueError as e:
print(f"Error generating motion profile: {e}")
exit()
except Exception as e:
print(f"An unexpected error occurred during profile generation: {e}")
exit()
# --- Calculate Trajectory Points and Joint Values ---
print("\n--- Calculating Inverse Kinematics for Trajectory ---")
# Initialize arrays to store results
xp = np.zeros(nt)
yp = np.zeros(nt)
zp = np.zeros(nt)
vxp = np.zeros(nt)
vyp = np.zeros(nt)
vzp = np.zeros(nt)
axp = np.zeros(nt)
ayp = np.zeros(nt)
azp = np.zeros(nt)
q1 = np.zeros(nt)
q2 = np.zeros(nt)
q3 = np.zeros(nt)
q4 = np.zeros(nt)
theta1 = np.zeros(nt)
theta2 = np.zeros(nt)
# Store R4 and rp for animation/plotting
R4_traj = [np.eye(3)] * nt
rp_traj = [np.zeros((3,1))] * nt
ik_errors = 0
start_time_ik = time.time()
for ntt in range(nt):
# Current displacement along the path
s_current = sc[ntt]
# Current angle on the circle (starts from -pi/2 direction based on MATLAB)
angle = s_current / r - (pi / 2) # Adjust start angle if needed
# Calculate target Cartesian position (matches MATLAB formulas)
xp[ntt] = r * np.cos(angle) # MATLAB uses sin(sc/r), which corresponds to cos(angle) here
yp[ntt] = r * np.sin(angle) + y_offset # MATLAB uses -r*cos(sc/r)-190 -> r*sin(angle)+y_offset
zp[ntt] = z_level
# Calculate target Cartesian velocity (using chain rule: d(pos)/dt = d(pos)/d(angle) * d(angle)/dt)
# d(angle)/dt = (d(angle)/ds) * (ds/dt) = (1/r) * vc[ntt]
v_current = vc[ntt]
vxp[ntt] = -r * np.sin(angle) * (v_current / r) # = -v_current * sin(angle)
vyp[ntt] = r * np.cos(angle) * (v_current / r) # = v_current * cos(angle)
vzp[ntt] = 0
# Check against MATLAB formulas:
# vxp = vc * cos(sc/r) -> vc * cos(angle+pi/2) = -vc * sin(angle). Matches.
# vyp = vc * sin(sc/r) -> vc * sin(angle+pi/2) = vc * cos(angle). Matches.
# Calculate target Cartesian acceleration (tangential + centripetal)
# Tangential acceleration vector: a_t = ac[ntt] * unit_tangent
# Centripetal acceleration vector: a_c = (vc[ntt]^2 / r) * unit_normal_inward
a_current = ac[ntt]
unit_tangent = np.array([-np.sin(angle), np.cos(angle), 0])
unit_normal_inward = np.array([-np.cos(angle), -np.sin(angle), 0]) # Points towards circle center (relative)
accel_tangential = a_current * unit_tangent
accel_centripetal = (v_current**2 / r) * unit_normal_inward if r > 1e-6 else np.zeros(3)
accel_total = accel_tangential + accel_centripetal
axp[ntt], ayp[ntt], azp[ntt] = accel_total[0], accel_total[1], accel_total[2]
# Check against MATLAB formulas:
# axp = ac*cos(sc/r) - (vc^2/r)*sin(sc/r) -> ac*(-sin(angle)) - (vc^2/r)*cos(angle). Matches tangential_x + centripetal_x.
# ayp = ac*sin(sc/r) + (vc^2/r)*cos(sc/r) -> ac*(cos(angle)) + (vc^2/r)*(-sin(angle)). Matches tangential_y + centripetal_y.
# Perform Inverse Kinematics
ik_result = inverse_kinematics(xp[ntt], yp[ntt], zp[ntt], robot_params)
if ik_result['error']:
print(f"IK Error at step {ntt}/{nt} (t={time_vec[ntt]:.3f}s): {ik_result['error']}")
ik_errors += 1
# Handle error: Use previous joint values? Stop? For now, store NaN or zeros.
q1[ntt], q2[ntt], q3[ntt], q4[ntt] = np.nan, np.nan, np.nan, np.nan
theta1[ntt], theta2[ntt] = np.nan, np.nan
# Keep previous R4 and rp for visualization continuity
if ntt > 0:
R4_traj[ntt] = R4_traj[ntt-1]
rp_traj[ntt] = rp_traj[ntt-1]
else:
R4_traj[ntt] = np.eye(3)
rp_traj[ntt] = np.array([[0],[0],[robot_params['e']]]) # Default start pose
else:
q1[ntt] = ik_result['q1']
q2[ntt] = ik_result['q2']
q3[ntt] = ik_result['q3']
q4[ntt] = ik_result['q4']
theta1[ntt] = ik_result['theta1']
theta2[ntt] = ik_result['theta2']
R4_traj[ntt] = ik_result['R4']
rp_traj[ntt] = ik_result['rp']
# Optional: Print progress
if (ntt + 1) % 100 == 0 or ntt == nt - 1:
print(f" Processed point {ntt+1}/{nt}")
end_time_ik = time.time()
print(f"IK calculation finished in {end_time_ik - start_time_ik:.2f} seconds.")
if ik_errors > 0:
print(f"Warning: Encountered {ik_errors} IK errors.")
# --- Calculate Joint Velocities (using Finite Differences) ---
print("\n--- Calculating Joint Velocities (Finite Differences) ---")
vq1 = np.zeros(nt)
vq2 = np.zeros(nt)
vq3 = np.zeros(nt)
# Forward difference for the first point, backward for the last, central for others
if nt > 1:
# Central difference for interior points
vq1[1:-1] = (q1[2:] - q1[:-2]) / (2 * profile_params['actual_ts'])
vq2[1:-1] = (q2[2:] - q2[:-2]) / (2 * profile_params['actual_ts'])
vq3[1:-1] = (q3[2:] - q3[:-2]) / (2 * profile_params['actual_ts'])
# Forward difference for the first point
vq1[0] = (q1[1] - q1[0]) / profile_params['actual_ts']
vq2[0] = (q2[1] - q2[0]) / profile_params['actual_ts']
vq3[0] = (q3[1] - q3[0]) / profile_params['actual_ts']
# Backward difference for the last point
vq1[-1] = (q1[-1] - q1[-2]) / profile_params['actual_ts']
vq2[-1] = (q2[-1] - q2[-2]) / profile_params['actual_ts']
vq3[-1] = (q3[-1] - q3[-2]) / profile_params['actual_ts']
elif nt == 1:
# Only one point, velocity is zero
pass # Already initialized to zero
# Handle NaNs from IK errors if any
vq1[np.isnan(q1)] = np.nan
vq2[np.isnan(q2)] = np.nan
vq3[np.isnan(q3)] = np.nan
print("Joint velocities calculated.")
# --- Plotting Results ---
print("\n--- Plotting Trajectory Results ---")
# 1. Cartesian Path Plot
fig_path = plt.figure(figsize=(8, 8))
ax_path = fig_path.add_subplot(111, projection='3d')
ax_path.plot(xp, yp, zp, 'b-', label='Desired Path')
ax_path.scatter(xp[0], yp[0], zp[0], c='g', marker='o', s=100, label='Start Point')
ax_path.scatter(xp[-1], yp[-1], zp[-1], c='r', marker='x', s=100, label='End Point')
ax_path.set_xlabel('X (mm)')
ax_path.set_ylabel('Y (mm)')
ax_path.set_zlabel('Z (mm)')
ax_path.set_title('Desired Cartesian Trajectory (Circular Path)')
# Make axes equal for better visualization of the circle
try:
ax_path.set_aspect('equal', adjustable='box')
except NotImplementedError:
# Fallback if equal aspect ratio fails
max_range = np.max([xp.max()-xp.min(), yp.max()-yp.min(), zp.max()-zp.min()])
mid_x = (xp.max()+xp.min())/2
mid_y = (yp.max()+yp.min())/2
mid_z = (zp.max()+zp.min())/2
ax_path.set_xlim(mid_x - max_range/2, mid_x + max_range/2)
ax_path.set_ylim(mid_y - max_range/2, mid_y + max_range/2)
ax_path.set_zlim(mid_z - max_range/2, mid_z + max_range/2)
ax_path.legend()
ax_path.grid(True)
plt.show(block=False)
# 2. Joint Space Plots
fig_joint, axs_joint = plt.subplots(2, 1, sharex=True, figsize=(10, 8))
# Joint Positions
axs_joint[0].plot(time_vec, q1, label='q1')
axs_joint[0].plot(time_vec, q2, label='q2')
axs_joint[0].plot(time_vec, q3, label='q3')
# axs_joint[0].plot(time_vec, q4, label='q4 (variable length)') # Optional
axs_joint[0].set_ylabel('Joint Position (mm or rad)')
axs_joint[0].set_title('Joint Space Trajectory')
axs_joint[0].grid(True)
axs_joint[0].legend()
# Joint Velocities
axs_joint[1].plot(time_vec, vq1, label='vq1')
axs_joint[1].plot(time_vec, vq2, label='vq2')
axs_joint[1].plot(time_vec, vq3, label='vq3')
axs_joint[1].set_ylabel('Joint Velocity (mm/s or rad/s)')
axs_joint[1].set_xlabel('Time (s)')
axs_joint[1].grid(True)
axs_joint[1].legend()
fig_joint.tight_layout()
plt.show(block=False)
# --- Animation (Optional) ---
print("\n--- Preparing Animation (Close other plots to start) ---")
input("Press Enter to start animation...")
fig_anim = plt.figure(figsize=(9, 9))
ax_anim = fig_anim.add_subplot(111, projection='3d')
plt.ion() # Turn on interactive mode
try:
# Determine plot limits based on the whole trajectory
all_rp = np.array([r.flatten() for r in rp_traj if r is not None and not np.isnan(r).any()])
if len(all_rp) > 0:
min_coords = np.min(all_rp, axis=0) - 200 # Add padding
max_coords = np.max(all_rp, axis=0) + 200 # Add padding
else: # Default limits if no valid points
min_coords = np.array([-500, -500, robot_params['e'] - 100])
max_coords = np.array([500, 500, robot_params['e'] + 1000])
# Fixed plot limits for animation stability
ax_anim.set_xlim(min(min_coords[0], -robot_params['b22']-100), max(max_coords[0], robot_params['b22']+100))
ax_anim.set_ylim(min(min_coords[1], -robot_params['b11']-100), max(max_coords[1], 100)) # Y often negative
ax_anim.set_zlim(0, max(max_coords[2], robot_params['e'] + 500))
for i in range(nt):
if np.isnan(q1[i]): # Skip frames with IK errors
print(f"Skipping frame {i} due to IK error.")
continue
rp_current = rp_traj[i]
R4_current = R4_traj[i]
plot_robot(ax_anim, rp_current, R4_current, robot_params,
title=f"Robot Trajectory Animation (t={time_vec[i]:.2f}s)")
# Draw trajectory path up to current point
ax_anim.plot(xp[:i+1], yp[:i+1], zp[:i+1], 'm:', lw=1, label='_Trajectory Path') # Underscore hides label repetition
plt.draw()
# Adjust pause time for desired animation speed
# Faster than real-time: pause_time = 0.01
# Closer to real-time: pause_time = max(0.001, ts - elapsed_draw_time)
plt.pause(0.01) # Small pause to allow rendering
# Stop animation if figure is closed
if not plt.fignum_exists(fig_anim.number):
print("Animation window closed by user.")
break
except KeyboardInterrupt:
print("\nAnimation interrupted by user.")
finally:
plt.ioff() # Turn off interactive mode
print("\nAnimation finished. Close the final plot window to exit.")
plt.show() # Keep the last frame visible