Primitives everywhere¶
In the previous sections, we have shown how to make a simple behavior thanks to the Robot
abstraction. But how to combine those elementary behaviors into something more complex? You could use threads and do it manually, but we provide the Primitive
to abstract most of the work for you.
What do we call “Primitive”?¶
We call Primitive
any simple or complex behavior applied to a Robot
. A primitive can access all sensors and effectors in the robot. A primitive is supposed to be independent of other primitives. In particular, a primitive is not aware of the other primitives running on the robot at the same time. We imagine those primitives as elementary blocks that can be combined to create more complex blocks in a hierarchical manner.
Note
The independence of primitives is really important when you create complex behaviors - such as balance - where many primitives are needed. Adding another primitive - such as walking - should be direct and not force you to rewrite everything. Furthermore, the balance primitive could also be combined with another behavior - such as shoot a ball - without modifying it.
To ensure this independence, the primitive is running in a sort of sandbox. More precisely, this means that the primitive has not direct access to the robot. It can only request commands (e.g. set a new goal position of a motor) to a PrimitiveManager
which transmits them to the “real” robot. As multiple primitives can run on the robot at the same time, their request orders are combined by the manager.
Note
The primitives all share the same manager. In further versions, we would like to move from this linear combination of all primitives to a hierarchical structure and have different layer of managers.
The manager uses a filter function to combine all orders sent by primitives. By default, this filter function is a simple mean but you can choose your own specific filter (e.g. add function).
Warning
You should not mix control through primitives and direct control through the Robot
. Indeed, the primitive manager will overwrite your orders at its refresh frequency: i.e. it will look like only the commands send through primitives will be taken into account.
Writing your own primitive¶
To write you own primitive, you have to subclass the Primitive
class. It provides you with basic mechanisms (e.g. connection to the manager, setup of the thread) to allow you to directly “plug” your primitive to your robot and run it.
Note
You should always call the super constructor if you override the __init__()
method.
As an example, let’s write a simple primitive that recreate the dance behavior written in the Controlling your Ergo-Robot section. Notice that to pass arguments to your primitive, you have to override the __init__()
method:
import time
import pypot.primitive
class DancePrimitive(pypot.primitive.Primitive):
def __init__(self, robot, amp=30, freq=0.5):
self.robot = robot
self.amp = amp
self.freq = freq
pypot.primitive.Primitive.__init__(self, robot)
def run(self):
amp = self.amp
freq = self.freq
# self.elapsed_time gives you the time (in s) since the primitive has been running
while self.elapsed_time < 30:
x = amp * numpy.sin(2 * numpy.pi * freq * self.elapsed_time)
self.robot.base_pan.goal_position = x
self.robot.head_pan.goal_position = -x
time.sleep(0.02)
To run this primitive on your robot, you simply have to do:
ergo_robot = pypot.robot.from_config(...)
dance = DancePrimitive(ergo_robot,amp=60, freq=0.6)
dance.start()
If you want to make the dance primitive infinite you can use the LoopPrimitive
class:
class LoopDancePrimitive(pypot.primitive.LoopPrimitive):
def __init__(self, robot, refresh_freq, amp=30, freq=0.5):
self.robot = robot
self.amp = amp
self.freq = freq
LoopPrimitive.__init__(self, robot, refresh_freq)
# The update function is automatically called at the frequency given on the constructor
def update(self):
amp = self.amp
freq = self.freq
x = amp * numpy.sin(2 * numpy.pi * freq * self.elapsed_time)
self.robot.base_pan.goal_position = x
self.robot.head_pan.goal_position = -x
And then runs it with:
ergo_robot = pypot.robot.from_config(...)
dance = LoopDancePrimitive(ergo_robot, 50, amp = 40, freq = 0.3)
# The robot will dance until you call dance.stop()
dance.start()
Warning
When writing your own primitive, you should always keep in mind that you should never directly pass the robot or its motors as argument and access them directly. You have to access them through the self.robot and self.robot.motors properties. Indeed, at instantiation the Robot
(resp. DxlMotor
) instance is transformed into a MockupRobot
(resp. MockupMotor
). Those class are used to intercept the orders sent and forward them to the PrimitiveManager
which will combine them. By directly accessing the “real” motor or robot you circumvent this mechanism and break the sandboxing. If you have to specify a list of motors to your primitive (e.g. apply the sinusoid primitive to the specified motors), you should either give the motors name and access the motors within the primitive or transform the list of DxlMotor
into MockupMotor
thanks to the get_mockup_motor()
method.
For instance:
class MyDummyPrimitive(pypot.primitive.Primitive):
def run(self, motors_name):
motors = [getattr(self.robot, name) for name in motors_name]
while True:
for m in fake_motors:
...
or:
class MyDummyPrimitive(pypot.primitive.Primitive):
def run(self, motors):
fake_motors = [self.get_mockup_motor(m) for m in motors]
while True:
for m in fake_motors:
...
Start, Stop, Pause, and Resume¶
The primitive can be start()
, stop()
, pause()
and resume()
. Unlike regular python thread, primitive can be restart by calling again the start()
method.
When overriding the Primitive
, you are responsible for correctly handling those events. For instance, the stop method will only trigger the should stop event that you should watch in your run loop and break it when the event is set. In particular, you should check the should_stop()
and should_pause()
in your run loop. You can also use the wait_to_stop()
and wait_to_resume()
to wait until the commands have really been executed.
Note
You can refer to the source code of the LoopPrimitive
for an example of how to correctly handle all these events.
Attaching a primitive to the robot¶
In the previous section, we explain that the primitives run in a sandbox in the sense that they are not aware of the other primitives running at the same time. In fact, this is not exactly true. More precisely, a primitive can access everything attached to the robot: e.g. motors, sensors. But you can also attach a primitive to the robot.
Let’s go back on our DancePrimitive example. You can write:
ergo_robot = pypot.robot.from_config(...)
ergo_robot.attach_primitive(DancePrimitive(ergo_robot), 'dance')
ergo_robot.dance.start()
By attaching a primitive to the robot, you make it accessible from within other primitive.
For instance you could then write:
class SelectorPrimitive(pypot.primitive.Primitive):
def run(self):
if song == 'my_favorite_song_to_dance' and not self.robot.dance.is_alive():
self.robot.dance.start()
Note
In this case, instantiating the DancePrimitive within the SelectorPrimitive would be another solution.