try mujoco2

This commit is contained in:
LiuzhengSJ
2026-05-29 14:21:03 +01:00
parent 8363b3fa6d
commit c23b5d0d6d

View File

@ -1,29 +1,29 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
""" """
RM75 Robot Controller with True Dynamics for URDF without Actuators Thread-based MuJoCo controller for kinematic verification
Run this in your coppeliasim conda environment No ROS dependency - pure Python threading
""" """
import mujoco import mujoco
import mujoco.viewer import mujoco.viewer
import numpy as np import numpy as np
import threading
import time import time
from pathlib import Path from pathlib import Path
from scipy.signal import butter, lfilter
class RM75Controller: class MuJoCoRobotController:
def __init__(self, urdf_path: str, enable_viewer: bool = True, """
control_mode='torque', # 'torque' or 'position' Thread-based robot controller for kinematic verification
low_pass_cutoff_hz=10.0): Command and feedback via thread-safe queues
"""
def __init__(self, urdf_path, smoothness=0.1, enable_viewer=True):
""" """
Initialize RM75 robot simulation with dynamic control
Args: Args:
urdf_path: Path to RM75-B.urdf file urdf_path: Path to URDF file
enable_viewer: Show visualization window smoothness: Motion smoothness (0.05-0.2)
control_mode: 'torque' (realistic dynamics) or 'position' (kinematic) enable_viewer: Show MuJoCo viewer
low_pass_cutoff_hz: Cutoff frequency for command filtering (Hz)
""" """
# Load model # Load model
self.model = mujoco.MjModel.from_xml_path(urdf_path) self.model = mujoco.MjModel.from_xml_path(urdf_path)
@ -31,335 +31,198 @@ class RM75Controller:
# Robot info # Robot info
self.n_joints = self.model.njnt self.n_joints = self.model.njnt
self.n_actuators = self.model.nu self.joint_names = [self.model.joint(i).name for i in range(self.n_joints)]
print(f"✓ Loaded RM75 robot") # Joint limits
print(f" - Joints: {self.n_joints}") self.joint_lower_limits = [self.model.jnt_range[i, 0] for i in range(self.n_joints)]
print(f" - Actuators in URDF: {self.n_actuators}") self.joint_upper_limits = [self.model.jnt_range[i, 1] for i in range(self.n_joints)]
print(f" - Bodies: {self.model.nbody}")
# Get joint names and limits print(f"Loaded robot: {self.n_joints} joints")
self.joint_names = []
self.joint_lower_limits = []
self.joint_upper_limits = []
for i in range(self.n_joints):
self.joint_names.append(self.model.joint(i).name)
# Get joint limits from URDF (range is [lower, upper])
self.joint_lower_limits.append(self.model.jnt_range[i, 0])
self.joint_upper_limits.append(self.model.jnt_range[i, 1])
print(f" - Joint limits: {self.joint_lower_limits[0]:.2f} to {self.joint_upper_limits[0]:.2f} rad for joint_1")
# If no actuators, we need to add them programmatically
if self.n_actuators == 0:
print(" No actuators in URDF. Adding torque actuators for dynamic control...")
self._add_actuators_programmatically()
# Control parameters # Control parameters
self.control_mode = control_mode self.smoothness = smoothness
# PD gains for torque control (tuned for realistic motion) self.j_cmd = self.data.qpos[:self.n_joints].copy()
self.kp = np.array([200.0, 200.0, 150.0, 100.0, 80.0, 60.0, 50.0]) # Proportional gains self.j_fbk = self.data.qvel[:self.n_joints].copy()
self.kd = np.array([20.0, 20.0, 15.0, 10.0, 8.0, 6.0, 5.0]) # Derivative gains
# State variables # Control flags
self.desired_positions = self.get_joint_positions().copy() self.running = False
self.simulation_thread = None
# Low-pass filter for smooth commands # Viewer
self.lp_cutoff = low_pass_cutoff_hz
self.filtered_commands = self.get_joint_positions().copy()
# Home position
self.home_position = self.get_joint_positions().copy()
print(f" - Home position: {self.home_position}")
# Setup viewer
self.viewer = None self.viewer = None
if enable_viewer: if enable_viewer:
try: try:
self.viewer = mujoco.viewer.launch_passive(self.model, self.data) self.viewer = mujoco.viewer.launch_passive(self.model, self.data)
print("✓ Viewer launched successfully")
except Exception as e: except Exception as e:
print(f"Warning: Could not launch viewer: {e}") print(f"Viewer warning: {e}")
self.viewer = None
def _add_actuators_programmatically(self): print("Robot controller ready")
"""Add torque actuators programmatically to enable dynamic control"""
# Save current model state
n_new_actuators = self.n_joints
# We need to create a new model with actuators def start(self):
# For now, we'll use direct qpos control with custom dynamics """Start the simulation thread"""
# This is a workaround by setting control_mode to 'position' for kinematic control if self.running:
# and using manual velocity/acceleration limits return
print(f" Adding {n_new_actuators} virtual torque actuators") self.running = True
self.simulation_thread = threading.Thread(target=self._simulation_loop, daemon=True)
self.simulation_thread.start()
print("Simulation thread started")
# Alternative approach: Use qfrc_applied to apply forces directly def stop(self):
# This bypasses the need for actuators in the URDF """Stop the simulation thread"""
self.use_qfrc_applied = True self.running = False
self.n_actuators = n_new_actuators if self.simulation_thread:
self.simulation_thread.join(timeout=2.0)
if self.viewer:
self.viewer.close()
print("Simulation stopped")
# Create virtual control array def send_command(self, joint_positions):
self.virtual_ctrl = np.zeros(self.n_joints)
def _low_pass_filter(self, new_target):
"""Apply simple first-order low-pass filter to target positions"""
dt = self.model.opt.timestep
alpha = 2 * np.pi * self.lp_cutoff * dt
alpha = min(alpha, 1.0) # Clamp for stability
for i in range(self.n_joints):
self.filtered_commands[i] = alpha * new_target[i] + (1 - alpha) * self.filtered_commands[i]
return self.filtered_commands.copy()
def get_joint_positions(self):
"""Get current joint angles (radians)"""
return self.data.qpos[:self.n_joints].copy()
def get_joint_velocities(self):
"""Get current joint velocities (rad/s)"""
return self.data.qvel[:self.n_joints].copy()
def set_desired_positions(self, target_positions, apply_filter=True):
""" """
Set desired joint positions Send joint command to robot (non-blocking)
Args: Args:
target_positions: Target joint angles in radians joint_positions: Array of target joint angles
apply_filter: Apply low-pass filter for smooth motion
""" """
target = np.array(target_positions[:self.n_joints]) cmd = np.array(joint_positions[:self.n_joints])
# Apply joint limits # Apply joint limits
for i in range(self.n_joints): for i in range(self.n_joints):
target[i] = np.clip(target[i], self.joint_lower_limits[i], self.joint_upper_limits[i]) cmd[i] = max(self.joint_lower_limits[i], min(self.joint_upper_limits[i], cmd[i]))
# Apply low-pass filter if enabled self.j_cmd = cmd
if apply_filter:
target = self._low_pass_filter(target)
self.desired_positions = target def get_feedback(self):
def compute_torques(self):
"""Compute torques using PD control with velocity damping"""
current_pos = self.get_joint_positions()
current_vel = self.get_joint_velocities()
# Position error
pos_error = self.desired_positions - current_pos
# PD control law (negative feedback)
torques = self.kp[:self.n_joints] * pos_error - self.kd[:self.n_joints] * current_vel
# Apply torque limits (safety)
max_torque = 50.0 # Nm limit
torques = np.clip(torques, -max_torque, max_torque)
return torques
def step(self):
"""Step the simulation with control applied"""
if self.control_mode == 'torque':
# Compute torques
torques = self.compute_torques()
# Apply torques directly to joints using qfrc_applied
# This bypasses the need for actuators in the URDF
self.data.qfrc_applied[:self.n_joints] = torques
elif self.control_mode == 'position':
# Direct position control (kinematic)
# Add velocity damping for smoother motion
current_pos = self.get_joint_positions()
pos_error = self.desired_positions - current_pos
# Simple proportional velocity control
kp_vel = 50.0
target_vel = kp_vel * pos_error
# Limit velocity
max_vel = 3.0 # rad/s
target_vel = np.clip(target_vel, -max_vel, max_vel)
# Apply velocity
self.data.qvel[:self.n_joints] = target_vel
# For position mode, we also apply small corrective torques
kp_correct = 100.0
kd_correct = 10.0
correction = kp_correct * pos_error - kd_correct * self.get_joint_velocities()
self.data.qfrc_applied[:self.n_joints] = correction
# Step physics
mujoco.mj_step(self.model, self.data)
# If using direct qpos control (position mode extreme)
if self.control_mode == 'position_direct':
self.data.qpos[:self.n_joints] = self.desired_positions
self.data.qvel[:self.n_joints] = 0
mujoco.mj_step(self.model, self.data)
# Sync viewer if active
if self.viewer:
self.viewer.sync()
def step_n(self, n_steps: int):
"""Advance simulation by N steps"""
for _ in range(n_steps):
self.step()
def move_to_position(self, target_positions, duration=1.0, apply_filter=True):
""" """
Smoothly move to target position with dynamics Get current robot state
Args: Returns:
target_positions: Target joint angles Dictionary with positions, velocities, etc.
duration: Movement duration (seconds)
apply_filter: Apply low-pass filtering
""" """
# Calculate number of steps for smooth interpolation return self.j_fbk
n_steps = int(duration / self.model.opt.timestep)
start_positions = self.get_joint_positions()
target = np.array(target_positions[:self.n_joints])
print(f" Moving with dynamics: {n_steps} steps over {duration}s")
for i in range(n_steps): def _simulation_loop(self):
# Linear interpolation for desired positions """Main simulation loop (runs in separate thread)"""
alpha = (i + 1) / n_steps last_time = time.time()
desired = start_positions + alpha * (target - start_positions)
# Set desired position (filter will apply smoothing) while self.running:
self.set_desired_positions(desired, apply_filter=apply_filter) # Process commands
# Get current state
current_pos = self.data.qpos[:self.n_joints].copy()
self.j_fbk = self.data.qpos[:self.n_joints].copy()
current_vel = self.data.qvel[:self.n_joints].copy()
# Smooth interpolation
alpha = self.smoothness
next_positions = current_pos + alpha * (self.j_cmd - current_pos)
# Calculate velocities for smooth motion
dt = self.model.opt.timestep
target_velocities = (next_positions - current_pos) / dt
# Limit velocities
max_vel = 3.0
target_velocities = np.clip(target_velocities, -max_vel, max_vel)
# Apply control
self.data.qvel[:self.n_joints] = target_velocities
# Corrective forces
pos_error = self.j_cmd - current_pos
kp = 30.0
self.data.qfrc_applied[:self.n_joints] = kp * pos_error
# Step simulation # Step simulation
self.step() mujoco.mj_step(self.model, self.data)
# Optional: print progress # Sync viewer
if (i + 1) % 200 == 0:
progress = (i + 1) / n_steps * 100
print(f" Progress: {progress:.0f}%")
def run_trajectory(self, trajectory_points, duration_per_point=1.0, apply_filter=True):
"""
Execute a trajectory with smooth dynamics
Args:
trajectory_points: List of joint position arrays
duration_per_point: Time per trajectory point (seconds)
apply_filter: Apply low-pass filtering
"""
print(f"\nExecuting trajectory with {len(trajectory_points)} points...")
for i, target in enumerate(trajectory_points):
print(f" Moving to point {i + 1}/{len(trajectory_points)}")
self.move_to_position(target, duration_per_point, apply_filter)
print("✓ Trajectory complete")
def run_interactive(self):
"""Run simulation with real-time control"""
print("\n✓ Simulation running with dynamic control")
print(f" Control mode: {self.control_mode}")
print(f" Low-pass cutoff: {self.lp_cutoff} Hz")
print(" Close viewer window to exit.\n")
try:
last_time = time.time()
while self.viewer and self.viewer.is_running():
self.step()
# Maintain real-time speed
elapsed = time.time() - last_time
sleep_time = self.model.opt.timestep - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
last_time = time.time()
except KeyboardInterrupt:
print("\n✓ Stopped by user")
finally:
if self.viewer: if self.viewer:
self.viewer.close() self.viewer.sync()
# Maintain real-time speed
elapsed = time.time() - last_time
sleep_time = self.model.opt.timestep - elapsed
if sleep_time > 0:
time.sleep(sleep_time)
last_time = time.time()
print(f'j_cmd: {self.j_cmd}, and j_fbk: {self.j_fbk}')
def print_state(self): def print_state(self):
"""Print current robot state""" """Print current robot state"""
positions = self.get_joint_positions() feedback = self.get_feedback()
velocities = self.get_joint_velocities() if feedback:
print(f"Positions (rad): {[f'{p:.3f}' for p in positions[:4]]}...") print(f"Positions: {[f'{p:.3f}' for p in feedback['positions'][:4]]}...")
print(f"Velocities (rad/s): {[f'{v:.3f}' for v in velocities[:4]]}...") print(f"Velocities: {[f'{v:.3f}' for v in feedback['velocities'][:4]]}...")
# Demo with different dynamic behaviors # Example usage for kinematic verification
if __name__ == "__main__": def verify_kinematics():
# Path to your URDF """Test sequence to verify kinematic code"""
urdf_file = "/home/zl/Downloads/urdf_rm75/RM75-B.urdf"
if not Path(urdf_file).exists(): # Create controller
print(f"Error: URDF file not found at {urdf_file}") urdf_path = "/home/zl/Downloads/urdf_rm75/RM75-B.urdf"
exit(1) robot = MuJoCoRobotController(urdf_path, smoothness=0.08, enable_viewer=True)
print("=" * 60) # Start simulation
print("RM75 Robot with Realistic Dynamics") robot.start()
print("=" * 60) time.sleep(1) # Wait for simulation to initialize
# Test 1: Torque control with low-pass filter (most realistic)
print("\n>>> Test 1: Torque Control with Low-Pass Filter (5 Hz)")
robot1 = RM75Controller(urdf_file, enable_viewer=True,
control_mode='torque',
low_pass_cutoff_hz=5.0) # Smooth, natural response
# Wait a moment for viewer to initialize
time.sleep(1)
# Move to a pose with dynamics
target_pose = robot1.home_position.copy()
target_pose[0] = 0.8 # Joint 1 (base rotation)
target_pose[1] = -0.5 # Joint 2 (shoulder)
target_pose[2] = 0.4 # Joint 3 (elbow)
target_pose[3] = 0.3 # Joint 4 (wrist 1)
print("\nMoving to target pose with realistic dynamics...")
robot1.move_to_position(target_pose, duration=2.0, apply_filter=True)
print("\nRobot state after movement:")
robot1.print_state()
# Test 2: Return to home with different filter
print("\n>>> Returning to home with softer filter (2 Hz)")
robot1.lp_cutoff = 2.0 # Change to slower response
robot1.move_to_position(robot1.home_position, duration=2.0, apply_filter=True)
# Test 3: Demonstrate different control modes
print("\n>>> Testing position control mode (faster response)")
robot2 = RM75Controller(urdf_file, enable_viewer=False,
control_mode='position',
low_pass_cutoff_hz=15.0)
test_pose = robot2.home_position.copy()
test_pose[0] = 0.5
test_pose[1] = -0.3
print("Moving in position control mode...")
robot2.move_to_position(test_pose, duration=1.0, apply_filter=True)
robot2.print_state()
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("Dynamic Behavior Summary") print("Kinematic Verification Test")
print("=" * 60) print("=" * 60)
print("✓ Torque Control: Realistic physics with inertia and damping")
print("✓ Low-pass filter: Adjustable smoothing (lower = smoother)")
print("✓ Joint limits: Automatically enforced from URDF")
print("✓ Programmatic actuation: No URDF modification needed")
print("\nTips for tuning:")
print(" - Lower cutoff (2-5 Hz): Smooth, natural motion")
print(" - Higher cutoff (10-20 Hz): More responsive")
print(" - Adjust kp/kd gains for stiffness/damping")
# Run interactive simulation # Test 1: Single joint movement
print("\nStarting interactive simulation...") print("\n[Test 1] Moving joint 1 to 45 degrees...")
print("Press Ctrl+C in terminal to exit, or close the viewer window.") cmd = np.zeros(7)
robot1.run_interactive() cmd[0] = 0.785 # 45 degrees
robot.send_command(cmd)
time.sleep(1)
feedback = robot.get_feedback()
print(f" Result: joint_1 = {feedback} rad (expected 0.785)")
# Test 2: Multi-joint pose
print("\n[Test 2] Moving to complex pose...")
cmd = np.array([0.5, -0.4, 0.3, 0.2, 0, 0, 0])
robot.send_command(cmd)
feedback = robot.get_feedback()
print(f" Result: {feedback}")
# Test 3: Return to home
print("\n[Test 3] Returning to home...")
robot.send_command(np.zeros(7))
feedback = robot.get_feedback()
print(f" Result: home = {feedback}")
# Test 4: Continuous trajectory for kinematic verification
print("\n[Test 4] Testing trajectory following...")
trajectory = [
np.array([0.3, 0, 0, 0, 0, 0, 0]),
np.array([0.6, 0, 0, 0, 0, 0, 0]),
np.array([0.3, 0, 0, 0, 0, 0, 0]),
np.array([0, 0, 0, 0, 0, 0, 0]),
]
for i, cmd in enumerate(trajectory):
print(f" Step {i + 1}: sending {cmd[0]:.3f}")
robot.send_command(cmd)
feedback = robot.get_feedback()
print(f" Reached: {feedback}")
print("\n✓ Kinematic verification complete!")
print("\nInteractive mode - send commands using robot.send_command()")
# Keep running for interactive control
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
print("\nShutting down...")
robot.stop()
if __name__ == "__main__":
verify_kinematics()