Bullet World

The BulletWorld is the environment in which all actions will be simulated. The BulletWorld class provides mainly methods controlling the simulation as well as getting objects that are present within the simulation. The BulletWorld uses the PyBullet library to realize the physics simulation and thus can be considered as a wrapper around the functionality of PyBullet.

Because PyBullet allows more than one physics client it is also possible with the BulletWorld. In other words, the user can have multiple instances of the BulletWorld,each with different scenarios.

A BulletWorld instance can be either of type direct or gui. With type direct there is no visible feedback of the actions in the simulation andthe simulation can only be affected by methods that change the status of an object.

In gui mode, however, there is a window in which the simulation is displayed and the objects can be dragged around with the mouse.

Because of technical restrictions it is only possible to have one graphical simulation, the rest will have to be executed in direct mode.

The following table lists all methods provided by the Bullet World class. There are methods to get objects from the BulletWorld either by their name, type or id, to set the gravity, and to simulate the BulletWorld for a given time. Furthermore, all event references are stored so that all events are unique for each instance of the BulletWorld. Currently, the only events available are for attachment, detachment, and manipulation.

Method Parameter Description
get_objects_by_name string Returns a list of all objects with the given name.
get_object_by_id int Returns the object corresponding to the given ID.
get_attachment_event None Returns the instance of the event which is called when two objects are attached.
get_detachment_event None Returns the instance of the event which is called when two objects are detached.
get_manipulation_event None Returns the instance of the event which is called when the environment was manipulated.
set_realtime Bool Sets the realtime in the simulation to True or False.
set_gravity list[float] Sets the gravity of the simulation to the given 3-dimensional vector.
simualte float Runs the simualtion for a given amount of seconds.
exit None Terminates the simulation.

The simulation in PyBullet works in steps, where every step equals approximately 1/240 seconds. Some methods of PyBullet need a simulation step to work: For example, the collision detection may only function if the step_simulation method was executed at least once before.

The simulate method takes the amount of seconds that should be simulated as a float, so that it is also possible to simulate fractions of a second. Next, the method loops the exact number of steps to simulate the given time.

Simulations can be ended using the exit method. This sets the current_bullet_world to an active BulletWorld and collects the threads. It is mainly used for working with multiple Bullet Worlds because problems may arise if one of the instances is not correctly terminated. Otherwise, the threads would not be collected and continue to allocate RAM. When working with only one it is not necessary to use the exit method, though it is recommended to do so.

Current Bullet World

The current_bullet_world variable is the default world for all methods. It will always point to the last initialized BulletWorld. To ensure that the current_bullet_world always points on a valid BulletWorld every, instance saves the previous current_bullet_world and resets it once it is finished; making this effectively a single linked list.

Object

The Object class represents anything inside the simulation regardless of whether it refers to a robot, the environment (like a kitchen) or an item (such as a mug). This makes the working with objects easier because one does not have to specify the na- ture of the given object. On the downside, this approach limits the functionality of the methods because the methods cannot be designed with a special use case in mind. Below you can see the constructor of the object class, the arguments and what they mean are being explained further down.

 Object (name, type, path, position = [ 0, 0, 0], orientation = [ 0, 0, 0, 1], world=None, color = [ 1, 1, 1, 1]) 

The constructor of the object class requires only the first three arguments, the rest is optional.

The first is the name of this object: it may be arbitrary as it is only used to identify the object.

The type of the object specifies the exact type of this object: For example, the type of a kitchen would be “environment”. This makes it simple to cluster objects and easily identify a specific group of objects. One instance in which this may be helpful is when excluding environmental objects, such as the kitchen or the floor, from reasoning queries.

The path argument is the path to the file, which describes this object. An object can either be generated from an URDF, stl or obj file. If the given file is of stl or obj format an URDF file with one link, that contains this file as mesh, will be generated and spawned. This needs to be performed as PyBullet does not directly support stl or obj files. In this process it is possible to define an individual color so that meshes can be spawned with different colors: For URDF files this is not necessary as the URDF format supports independent colors for every link.

The position and orientation arguments define the position and orientation in which the object should be spawned. These are represented as lists of x, y, z in world coordinate frame for the position and as a list representing a quaternion for the orientation. A quaternion is a list of 4 numeric values that describe the rotation around each axis as well as a normalization value.

The world argument specifies a BulletWorld in which the object should be spawned. If no world is given, the current_bullet_world is used. The argument is set to None and not directly to the reference because the standard values will only be evaluated once and then be set. This is problematic when dealing with object references instead of static values. As a workaround for this problem the current_bullet_world will be set in the constructor if the world argument is None.

The color argument only works when an obj or stl file is provided. The color is given as a list of RGBA.

Methods

The object class provides a number of methods to interact with this object. The current position and orientation of the object or it's links can be queried, it can be attached to other objects or the position, orientation or joint values can be changed.

Method Parameter Description
attach Object, link (optional), loose (optional) Attaches this object to the other given object.
detach Object Detaches this object from the other given object. This method only works if they were previously attached.
get_position None Returns the position of this object in world coordinate frame.
get_pose None An alias for get_position.
get_orientation None Returns the orientation of this object as quanternion.
get_position_and_orientation None Returns the position and orientation of this object as a list with position in xyz and orientation as a quanternion, both in world coordinate frame.
set_position_and_orientation list[float], list [float] Sets the position and orientation of this object.
set_position list[float] Sets the base position of this object, the position must be given as a list of xyz.
set_orientation list[float] Sets the orientation of this object, the position must be given as a list representing a quanternion.
set_joint_state String, float Sets the joint value of the given joint name to the given value.
get_joint_id String Returns the unique ID of the given joint name.
get_link_id String Returns the unique ID of the given link name.
get_link_position String Returns the position of a given link of this object, the link is specified by its name.
get_link_orientation String Returns the orientation of a given link of this object, the link is specified by its name.
get_link_position_and_orientation String Returns the position and orientation of a given link as a list.
get_joint_state String Returns the joint value of the given joint name.

Attachments

The attachments are mainly used to simulate pick-up actions, because the robot and the object it picks up are two independent objects. As they are also simulated independently, attachments are needed. The attachment method saves the attached object into an internal dictionary which keeps track of all attachments, in addition a virtual joint will be created between the two attached objects. This virtual joints will only be resolved by PyBullet if the simulation is running and is thus only for this purpose.

Because the simulation is rarely run when the robot moves, he is teleported between positions the save time, there is a second method _set_attached_objects which updates the position of all attached objects and objects which are attached to these objects recursively. The _set_attached_objects method is called every time the joint states, position or orientation are changed.

These two methods of attachments ensure that the relative position between objects is maintained at all times.

To keep track of all attachments, every object has a dictionary with the attached object as keys and the unique id of the attachment as value. The id is returned by the PyBullet method which creates the virtual joint. This dictionary is identical for both objects of the attachment, meaning, it is irrelevant on which object the detachment method is called because both know the unique id of the attachment.

To attach two objects to another, the user just needs to call the attach method on one of the objects and provide the other object as a parameter.

If robot and fork are the names of the objects, the attach method looks like this:

 robot.attach(fork) 

This would be enough to attach the objects, but without further parameters the attachment would be between the base position of the robot and the fork. This is not ideal as a robot would hold a fork with its hands and we want to simulate the attachment this way. For that purpose it is possible to specify the links between which the attachment should be created. This would look like this:

 robot.attach(fork, link="gripper name", loose=false)

Lastly, it is important to mention is that it is not possible to call the attach method again if there is already an attachment between the given objects. The reason for this is that it would create a second joint between the objects and override the entry in the dictionary of all attachments and create an irremovable joint. If the user were to call the attach method with an existing attachment, it would just return without having altered anything.

Loose Attachments

Loose attachments are a special case of attachments in which the position of the loose attached object is not updated. This is used to create unidirectional attachments in contrast to the regular attachments which are bidirectional. This has a use case, for example, if a bottle is placed on a tray, the bottle should move with the tray but the tray should stay at the same position if the bottle is moved.

Detachments

The detachments can be considered straight-forward because only the virtual joint needs to be removed and the attachment needs to be deleted from the dictionary. With the removal, the two objects become again independent of one another.

To detach two objects, the user just needs to call the detach method on one object and provide the other object as a parameter.

 robot.detach(fork) 

Shadow Worlds

Since some methods especially the geometric reasoning need to modify the position of objects it is necessary to have a second Bullet World, so the second Bullet World can be modified without changing the state of the main Bullet World. This is mainly required to have a consistent belief state. This is what shadow worlds provide.

A shadow world is a second Bullet World that mirrors the main Bullet World, so everything that happens in the main Bullet World will be reflected in the shadow world. This happens completely autonomous by the World_Sync class, this class runs in a separate thread and spawns, removes and changes the location of objects in the shadow world.

World Sync

The World Sync synchronizes the shadow world with the main Bullet World, it basically provides the “shadow” functionality. This module runs in a seperate thread and checks every 0.1 seconds if there are objects that need to be added or removed from the shadow world. Checking if anything should be added or removed is done by two queues, anytime an object in the main Bullet World is added or removed there will be a new entry added to the queue with every necessary information. The World Sync then checks if there are any entries in the queue and processes them.

There is also an object mapping which maps the objects of the main Bullet World to the corresponding objects in the shadow world.

Using the shadow world

To use the shadow world you have to import Use_shadow_world from pycram.bullet_world and use it in a “with” environment, in this environment you can use the BulletWorld.current_bullet_world variable which points at the shadow world.

There are also two methods for retrieving the corresponding object of the other world, these are get_shadow_object and get_bullet_object_for_shadow.

from pycram.bullet_world import BulletWorld, Object, Use_shadow_world 

world = BulletWorld()
milk = Object("milk", "milk", "milk.stl")
shadow_milk = world.get_shadow_object(milk)

with Use_shadow_world():
    shadow_milk.set_position([0,0, 1])
    BulletWorld.current_bullet_world.simulate(10)