Use PID to drive the robot¶
Note
If you haven’t already done so, make sure you’ve completed the steps in Getting Started before this tutorial.
Basic setup¶
This part is similar as before with only few parameter change. In this tutorial, we will show how to drive your robot to the target position(same as previous tutorial) use Physx’s internal PD or write a PID by yourself.
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 | import sapien.core as sapien
import numpy as np
def robot_basic_control_demo(use_internal_pd, use_external_pid):
sim = sapien.Engine()
renderer = sapien.OptifuserRenderer()
renderer.enable_global_axes(False)
sim.set_renderer(renderer)
renderer_controller = sapien.OptifuserController(renderer)
renderer_controller.set_camera_position(-2, 0, 1)
renderer_controller.set_camera_rotation(0, -0.5)
scene0 = sim.create_scene(gravity=[0, 0, -9.81])
renderer_controller.set_current_scene(scene0)
scene0.add_ground(altitude=0)
scene0.set_timestep(1 / 2000)
scene0.set_ambient_light([0.5, 0.5, 0.5])
scene0.set_shadow_light([0, 1, -1], [0.5, 0.5, 0.5])
loader = scene0.create_urdf_loader()
loader.fix_root_link = True
robot = loader.load("assets/robot/jaco2.urdf")
arm_init_qpos = [4.71, 2.84, 0, 0.75, 4.62, 4.48, 4.88]
arm_zero_qpos = [0, 3.14, 0, 3.14, 0, 3.14, 0]
gripper_init_qpos = [0, 0, 0, 0, 0, 0]
target_pos = np.array(arm_init_qpos + gripper_init_qpos)
zero_qpos = arm_zero_qpos + gripper_init_qpos
robot.set_qpos(zero_qpos)
|
Drive robot with Physx’s internal PD controller¶
To use Physx’s internal drive, you can use the joint.set_drive_property
function for each joint.
The stiffness and damping can be regarded as P
and D
in a typical PID controller.
Then, you can use set_drive_target
to set the target position of either a single joint or a whole robot.
Then the internal solver in Physx will calculate the joint force in order to reach this position.
Note
If you do not balance the passive force, e.g. gravity, you can never reach the desired pose given in set_drive_target
due to steady-state-error.
69 70 71 72 73 | active_joints = [joint for joint in robot.get_joints() if joint.get_dof() > 0]
for i, joint in enumerate(active_joints):
if use_internal_pd:
joint.set_drive_property(stiffness=5, damping=5)
robot.set_drive_target(target_pos)
|
Write your own PID controller¶
If you really need the integrator I
to implement a PID controller, you can write it by your own.
There is a very simple PID controller as follow, where parameters are not fine-tuned. You can add more
tricks when doing integration or propagate the error to make your controller more stable.
In most cases, it is always recommended to use the set_drive_target
instead of your own PID. The Physx function is much more efficient and robustness when the parameters are not well-tuned.
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class SimplePID:
def __init__(self, kp=0.0, ki=0.0, kd=0.0):
self.p = kp
self.i = ki
self.d = kd
self._cp = 0
self._ci = 0
self._cd = 0
self._last_error = 0
def compute(self, current_error, dt):
d_error = current_error - self._last_error
self._cp = current_error
self._ci += current_error * dt
if abs(self._last_error) > 0.01:
self._cd = d_error / dt
self._last_error = current_error
signal = (self.p * self._cp) + (self.i * self._ci) + (self.d * self._cd)
return signal
|
65 66 67 68 69 70 71 72 73 74 | pids = []
pid_parameters = [(40, 5, 2), (40, 5.0, 2), (40, 5.0, 2), (20, 5.0, 2), (5, 0.8, 2), (5, 0.8, 2),
(5, 0.8, 0.4), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02),
(0.1, 0, 0.02)]
active_joints = [joint for joint in robot.get_joints() if joint.get_dof() > 0]
for i, joint in enumerate(active_joints):
if use_internal_pd:
joint.set_drive_property(stiffness=5, damping=5)
if use_external_pid:
pids.append(SimplePID(pid_parameters[i][0], pid_parameters[i][1], pid_parameters[i][2]))
|
79 80 81 82 83 84 85 86 87 88 | while not renderer_controller.should_quit:
scene0.update_render()
for i in range(4):
qf = robot.compute_passive_force(gravity=True, coriolis_and_centrifugal=True, external=True)
if use_external_pid:
pid_qf = pid_forward(pids, target_pos, robot.get_qpos(), scene0.get_timestep())
qf += pid_qf
robot.set_qf(qf)
scene0.step()
steps += 1
|
Note
In practice, not use your own PID unless you really need to balance some steady-state-error which can not be compensated by compensate_passive_force
.
The entire code¶
The entire code is as follow:
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 | import sapien.core as sapien
import numpy as np
class SimplePID:
def __init__(self, kp=0.0, ki=0.0, kd=0.0):
self.p = kp
self.i = ki
self.d = kd
self._cp = 0
self._ci = 0
self._cd = 0
self._last_error = 0
def compute(self, current_error, dt):
d_error = current_error - self._last_error
self._cp = current_error
self._ci += current_error * dt
if abs(self._last_error) > 0.01:
self._cd = d_error / dt
self._last_error = current_error
signal = (self.p * self._cp) + (self.i * self._ci) + (self.d * self._cd)
return signal
def pid_forward(pids: list, target_pos: np.ndarray, current_pos: np.ndarray, dt: float) -> np.ndarray:
qf = np.zeros(len(pids))
errors = target_pos - current_pos
for i in range(len(pids)):
qf[i] = pids[i].compute(errors[i], dt)
return qf
def robot_basic_control_demo(use_internal_pd, use_external_pid):
sim = sapien.Engine()
renderer = sapien.OptifuserRenderer()
renderer.enable_global_axes(False)
sim.set_renderer(renderer)
renderer_controller = sapien.OptifuserController(renderer)
renderer_controller.set_camera_position(-2, 0, 1)
renderer_controller.set_camera_rotation(0, -0.5)
scene0 = sim.create_scene(gravity=[0, 0, -9.81])
renderer_controller.set_current_scene(scene0)
scene0.add_ground(altitude=0)
scene0.set_timestep(1 / 2000)
scene0.set_ambient_light([0.5, 0.5, 0.5])
scene0.set_shadow_light([0, 1, -1], [0.5, 0.5, 0.5])
loader = scene0.create_urdf_loader()
loader.fix_root_link = True
robot = loader.load("assets/robot/jaco2.urdf")
arm_init_qpos = [4.71, 2.84, 0, 0.75, 4.62, 4.48, 4.88]
arm_zero_qpos = [0, 3.14, 0, 3.14, 0, 3.14, 0]
gripper_init_qpos = [0, 0, 0, 0, 0, 0]
target_pos = np.array(arm_init_qpos + gripper_init_qpos)
zero_qpos = arm_zero_qpos + gripper_init_qpos
robot.set_qpos(zero_qpos)
pids = []
pid_parameters = [(40, 5, 2), (40, 5.0, 2), (40, 5.0, 2), (20, 5.0, 2), (5, 0.8, 2), (5, 0.8, 2),
(5, 0.8, 0.4), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02), (0.1, 0, 0.02),
(0.1, 0, 0.02)]
active_joints = [joint for joint in robot.get_joints() if joint.get_dof() > 0]
for i, joint in enumerate(active_joints):
if use_internal_pd:
joint.set_drive_property(stiffness=5, damping=5)
if use_external_pid:
pids.append(SimplePID(pid_parameters[i][0], pid_parameters[i][1], pid_parameters[i][2]))
robot.set_drive_target(target_pos)
steps = 0
renderer_controller.show_window()
while not renderer_controller.should_quit:
scene0.update_render()
for i in range(4):
qf = robot.compute_passive_force(gravity=True, coriolis_and_centrifugal=True, external=True)
if use_external_pid:
pid_qf = pid_forward(pids, target_pos, robot.get_qpos(), scene0.get_timestep())
qf += pid_qf
robot.set_qf(qf)
scene0.step()
steps += 1
renderer_controller.render()
scene0 = None
if __name__ == '__main__':
robot_basic_control_demo(False, True)
|