目前,验证连续控制强化学习算法的仿真环境,最常用的是Mujoco,但是需要license,学生邮箱可申请单机一年的免费使用权。同时,大部分情况下,如果使用Gym和Python,还需要安装Mujoco-py,安装过程都不友好。另外很重要的是,据个人了解,Mujoco原生版,也就是C++版的文档写的水平一般,Mujoco-py本身就没什么文档,只有几个example。

相比之下,PyBullet使用免费的Bullet引擎,安装过程也仅是pip install pybullet这么简单。PyBullet的文档也是良心所在,文档几乎面面俱到,文档地址PyBullet Quickstart Guide,同时,每个API都提供了详细的example,还有PyBullet应用在Gym、强化学习算法、机器人等特殊目的的example。对于想用Bullet引擎做游戏和视频的小伙伴,Bullet的每个C++ API也是提供了详细的example。

总结起来就是,PyBullet上手难度更低,国外有博主说明年(2021年)PyBullet有可能会成为机器人强化学习领域的仿真环境的事实标准。

目前,在用PyBullet做仿真的强化学习项目越来越多,像
Deep Mimic

Deep Mimic
Agile Robottic Locomotion
在这里插入图片描述
Table Tennis Robot
在这里插入图片描述
tossingBot
在这里插入图片描述
和很多很多其他的项目。
同时,也有很多项目是用的Mujoco,具体可参考Reinforcement learning for robotics applications

一波推销过后,说回正题,下面将一步步的说明pybulelt的使用。
我们将以Panda机器人为例。
在这里插入图片描述

PyBulelt使用URDF解析运动学、形状、动力学等特性。同时也支持Gazebo的SDF、Mujoco的MJCF。在pybullet_data文件夹,已经定义了很多机器人的描述文件。
在这里插入图片描述
在这里插入图片描述
我们首先在环境中添加一个机器人。我们需要os模块来寻找目录中的文件;pybullet.connect(pybullet.GUI)用来启动仿真;pybullet.stepSimulation()运行一步仿真。

import os
import pybullet as p
import pybullet_data as pd

p.connect(p.GUI)
pandaUid=p.loadURDF(os.path.join(pybullet_data.getDataPath(),"franka_panda/panda.urdf"),useFixedBase=True)

while True:
    p.stepSimulation()

当然如果不用os模块,也可以使用p.setAditionalSearchPath()这个函数来做。

import pybullet as p
import pybullet_data as pd

p.connect(p.GUI)
p.setAdditionalSearchPath(pd.getDataPath())
pandaUid=p.loadURDF("franka_panda/panda.urdf",useFixedBase=True)
while True:
    p.stepSimulation()

结果就是有个机器人出现了。

在这里插入图片描述


那为什么os模块和setAdditionalSearchPath()都可以实现相同的效果呢?
原因很简单。因为他俩做的是同样的工作。有点废话废键盘。

在Pycharm中,我们在import pybullet_data as pd这句的pybullet_data上面按Ctrl+B,将会跳转到pybullet_data的定义上面。可以看到打开的是__init__.py文件。
在这里插入图片描述
哎,这不和上面用os的时候一样的操作吗。就是把pybullet_data文件夹添加进来。

同样,要注意到一个奇怪的现象,看看pybullet_data的安装位置。为什么没有安装在anaconda的env的site-package里面? 我也不知道,已经提交issue了(2020-8-30)。
issue erwin大神已经回了. 链接
在这里插入图片描述


我们打开pybullet_data文件夹看一下目录结构。里面全都是描述文件。
在这里插入图片描述

其实说到pybullet_data文件夹的位置比较奇怪,那看看pybullet的位置,那简直叫离奇了。在import pybullet as p的pybullet上按Ctrl+B,跳转到了pybullet.py,看位置。

C:\Users\born_\AppData\Local\JetBrains\PyCharm2020.2\python_stubs\-520983747

什么玩意??黑人问号走起来。
pybullet.py中全是函数的定义,函数体都是pass,说明只是暴漏在外的API,还有封装好的文件。
看pybullet.py的前面几行注释,其中有一行
在这里插入图片描述

可以看到封装好的文件就是这个。pybullet的函数体就定义在里面。


下面,我们将机器人放在桌子上。在pandaUid下面添加这句。

tableUid=p.loadURDF("table/table.urdf",basePosition=[0.5,0,-0.65])

再添加一个盒子

trayUid=p.loadURDF("tray/traybox.urdf",basePosition=[0.65,0,0])

最后的代码就是

import pybullet as p
import pybullet_data as pd

p.connect(p.GUI)
p.setAdditionalSearchPath(pd.getDataPath())
pandaUid=p.loadURDF("franka_panda/panda.urdf",useFixedBase=True)
tableUid=p.loadURDF("table/table.urdf",basePosition=[0.5,0,-0.65])
trayUid=p.loadURDF("tray/traybox.urdf",basePosition=[0.65,0,0])
while True:
    p.stepSimulation()

在这里插入图片描述
所有的模型都可以在pybullet_env文件夹(文件夹的位置上面已经说到了)找到(可以将pybullet的Github仓库clone下来浏览一下目录结构)。

文件夹中全是.urdf或者其他描述文件,没办法一下子看出模型长什么样,除非把他们用loadURDF()添加进来查看。但是呢,有更好的办法,走起,不通过ROS直接查看URDF文件


我们继续添加模型。

objectUid=p.loadURDF("random_urdfs/000/000.urdf",basePosition=[0.7,0,0.7])

在这里插入图片描述

咋飘着? 模型飘了? 没加重力而已。

p.setGravity(0,0,-10)

p.connect(p.GUI)下面添加这句就行。


上面的截图肯定和刚运行出来的时候不一样,我是拉近了不知道多少才能看到这样,不然显示的效果离自己很远很小。

咋办?
调整视窗白。

p.resetDebugVisualizerCamera(cameraDistance=1.5,cameraYaw=0,\
                             cameraPitch=-40,cameraTargetPosition=[0.55,-0.35,0.2])

当然也可以自己调整一下参数值,看一下效果。比如这样也是可以的

p.resetDebugVisualizerCamera(cameraDistance=1,cameraYaw=0,\
                             cameraPitch=-40,cameraTargetPosition=[0.5,-0.9,0.5])

参数中cameraTargetPosition指摄像机的位置,cameraDistance指相机的焦距的变化,类似变焦。

完整代码

import pybullet as p
import pybullet_data as pd

# cameraTargetPosition=[0.55,-0.35,0.2]
p.connect(p.GUI)
p.setGravity(0,0,-10)
# p.configureDebugVisualizer(p.COV_ENABLE_SINGLE_STEP_RENDERING)
p.setAdditionalSearchPath(pd.getDataPath())
p.resetDebugVisualizerCamera(cameraDistance=1,cameraYaw=0,\
                             cameraPitch=-40,cameraTargetPosition=[0.5,-0.9,0.5])

pandaUid=p.loadURDF("franka_panda/panda.urdf",useFixedBase=True)
tableUid=p.loadURDF("table/table.urdf",basePosition=[0.5,0,-0.65])
trayUid=p.loadURDF("tray/traybox.urdf",basePosition=[0.65,0,0])

objectUid=p.loadURDF("random_urdfs/000/000.urdf",basePosition=[0.7,0,0.7])
object1Uid=p.loadURDF("random_urdfs/001/001.urdf",basePosition=[0.7,0.1,0.7])
object2Uid=p.loadURDF("random_urdfs/002/002.urdf",basePosition=[0.6,-0.2,0.7])
object3Uid=p.loadURDF("random_urdfs/003/003.urdf",basePosition=[0.8,0.1,0.7])
object4Uid=p.loadURDF("random_urdfs/004/004.urdf",basePosition=[0.7,-0.1,0.7])
object5Uid=p.loadURDF("random_urdfs/005/005.urdf",basePosition=[0.6,0.1,0.3])
object6Uid=p.loadURDF("random_urdfs/006/006.urdf",basePosition=[0.8,-0.2,0.4])
object7Uid=p.loadURDF("random_urdfs/007/007.urdf",basePosition=[0.6,0.2,0.4])
object8Uid=p.loadURDF("random_urdfs/008/008.urdf",basePosition=[0.7,-0.1,0.4])
object9Uid=p.loadURDF("random_urdfs/009/009.urdf",basePosition=[0.6,0.1,0.4])


while True:
    p.stepSimulation()

下面,我们来控制机械臂运动。
控制电机的API是setJointMotorControl2()。在Panda机器人的URDF定义里,joint 0~6是关节电机,9, 10是夹爪电机。

我们将机械臂的抓取过程分成4个步骤:current_state==0时,将机械臂移动到物体的上面,打开夹爪;current_state==1时,往下移动夹爪;current_state==3时,夹住物体。

每个步骤经过1个单位时间,即比如设置setTimeStep=1./240.时,经过240个stepSimulation()完成当前步骤。

完整代码:

import pybullet as p
import pybullet_data as pd
import math
import time
# cameraTargetPosition=[0.55,-0.35,0.2]
p.connect(p.GUI)
p.setGravity(0,0,-10)
# p.configureDebugVisualizer(p.COV_ENABLE_SINGLE_STEP_RENDERING)
p.setAdditionalSearchPath(pd.getDataPath())
p.resetDebugVisualizerCamera(cameraDistance=1,cameraYaw=0,\
                             cameraPitch=-40,cameraTargetPosition=[0.5,-0.9,0.5])

pandaUid=p.loadURDF("franka_panda/panda.urdf",useFixedBase=True)
tableUid=p.loadURDF("table/table.urdf",basePosition=[0.5,0,-0.65])
trayUid=p.loadURDF("tray/traybox.urdf",basePosition=[0.65,0,0])

objectUid=p.loadURDF("random_urdfs/000/000.urdf",basePosition=[0.7,0,0.1])
# object1Uid=p.loadURDF("random_urdfs/001/001.urdf",basePosition=[0.7,0.1,0.7])
# object2Uid=p.loadURDF("random_urdfs/002/002.urdf",basePosition=[0.6,-0.2,0.7])
# object3Uid=p.loadURDF("random_urdfs/003/003.urdf",basePosition=[0.8,0.1,0.7])
# object4Uid=p.loadURDF("random_urdfs/004/004.urdf",basePosition=[0.7,-0.1,0.7])
# object5Uid=p.loadURDF("random_urdfs/005/005.urdf",basePosition=[0.6,0.1,0.3])
# object6Uid=p.loadURDF("random_urdfs/006/006.urdf",basePosition=[0.8,-0.2,0.4])
# object7Uid=p.loadURDF("random_urdfs/007/007.urdf",basePosition=[0.6,0.2,0.4])
# object8Uid=p.loadURDF("random_urdfs/008/008.urdf",basePosition=[0.7,-0.1,0.4])
# object9Uid=p.loadURDF("random_urdfs/009/009.urdf",basePosition=[0.6,0.1,0.4])

state_durations=[1,1,1,1]
control_dt=1./240.
p.setTimeStep=control_dt
state_t=0.
current_state=0

while True:

    state_t+=control_dt
    #p.configureDebugVisualizer(p.COV_ENABLE_SINGLE_STEP_RENDERING)

    if current_state==0:
        p.setJointMotorControl2(pandaUid,0,p.POSITION_CONTROL,0)
        p.setJointMotorControl2(pandaUid,1,p.POSITION_CONTROL,math.pi/4.)
        p.setJointMotorControl2(pandaUid,2,p.POSITION_CONTROL,0)
        p.setJointMotorControl2(pandaUid,3,p.POSITION_CONTROL,-math.pi/2.)
        p.setJointMotorControl2(pandaUid,4,p.POSITION_CONTROL,0)
        p.setJointMotorControl2(pandaUid,5,p.POSITION_CONTROL,3*math.pi/4)
        p.setJointMotorControl2(pandaUid,6,p.POSITION_CONTROL,-math.pi/4.)
        p.setJointMotorControl2(pandaUid,9,p.POSITION_CONTROL,0.08)
        p.setJointMotorControl2(pandaUid,10,p.POSITION_CONTROL,0.08)

    if current_state==1:
        p.setJointMotorControl2(pandaUid,1,p.POSITION_CONTROL,math.pi/4.+.15)
        p.setJointMotorControl2(pandaUid,3,p.POSITION_CONTROL,-math.pi/2.+.15)

    if current_state==2:
        p.setJointMotorControl2(pandaUid,9,p.POSITION_CONTROL,force=200)
        p.setJointMotorControl2(pandaUid,10,p.POSITION_CONTROL,force=200)

    if current_state==3:
        p.setJointMotorControl2(pandaUid,1,p.POSITION_CONTROL,math.pi/4.-1)
        p.setJointMotorControl2(pandaUid,3,p.POSITION_CONTROL,-math.pi/2.-1)

    if state_t>state_durations[current_state]:
        current_state+=1
        if current_state>=len(state_durations):
            current_state=0
        state_t=0

    p.stepSimulation()

运行代码你会发现,除了机械臂是在突变位置外,其他一切正常。机械臂咋会突变位置呢?

这就牵扯到一个API:

p.configureDebugVisualizer(p.COV_ENABLE_SINGLE_STEP_RENDERING)

p.COV_ENABLE_SINGLE_STEP_RENDERING是指允许机械臂慢慢的渲染。

while True:下面的第二句,把注释取消掉,就可以看到缓慢渲染的过程了。