vulp
2.3.0
|
Vulp provides an action-observation loop to control robots from a standalone "agent" process, like this:
The agent can be a simple Python script with few dependencies.
Vulp is designed for robots built with the mjbots stack (moteus servo controllers and pi3hat communication board). It provides a robot/simulation switch to train or test agents in Bullet before running them on the real system.
Vulp supports Linux and macOS for development, and Raspberry Pi OS for robot deployment.
Check out the upkie
repository for an example where Vulp is used to implement simulation environments, real-robot spines, state observers and locomotion agents.
More accurately, Vulp is a tiny inter-process communication (IPC) protocol shipped with reference libraries (currently in Python and C++, other languages welcome). It is suitable for tasks that require real-time but not high-frequency performance. The main use case for this is balancing, as there is theoretical and empirical evidence suggesting that bipeds and quadrupeds can balance themselves as leisurely as 5–15 Hz, although balance control is frequently implemented at 200–1000 Hz. And if you are wondering whether Python is suitable for real-time applications, we were too! Until we tried it out.
In Vulp, a fast program, called a spine, talks to a slow program, called an agent, in a standard action-observation loop. Spine and agent run in separate processes and exchange action
and observation
dictionaries through shared memory. For instance, action
can be a set of joint commands and observation
a set of joint observations. Vulp provides a pipeline API to grow more complex spines with additional controllers (for higher-level actions) and observers (for richer observations). For example, a spine can run an inverse kinematics solver, or output its own ground contact estimation.
All design decisions have their pros and cons. Take a look at the features and non-features below to decide if Vulp is a fit to your use case.
If any of the non-features is a no-go to you, you may also want to check out these existing alternatives:
ControllerInterface
implementing the dictionary-based IPC protocol.If your robot is built with some of the following open hardware components, you can also use their corresponding Python bindings directly:
Using control bindings directly is a simpler alternative if you don't need the action-observation loop and simulation/real-robot switch from Vulp.
Python agents talk with Vulp spines via the SpineInterface
, which can process both actions and observations in about 0.7 ± 0.3 ms. This leaves plenty of room to implement other control components in a low-frequency loop. You may also be surprised at how Python performance has improved in recent years (most "tricks" that were popular ten years ago have been optimized away in CPython 3.8+). To consider one data point, here are the cycle periods measured in a complete Python agent for Upkie (the Pink balancer from upkie
) running on a Raspberry Pi 4 Model B (Quad core ARM Cortex-A72 @ 1.5GHz). It performs non-trivial tasks like balancing and whole-body inverse kinematics by quadratic programming:
Note that the aforementioned 0.7 ± 0.3 ms processing time happens on the Python side, and is thus included in the 5.0 ms cycles represented by the orange curve. Meanwhile the spine is set to a reference frequency of 1.0 kHz and its corresponding cycle period was measured here at 1.0 ± 0.05 ms.
Make sure you switch Bazel's compilation mode to "opt" when running both robot experiments and simulations. The compilation mode is "fastbuild" by default. Note that it is totally fine to compile agents in "fastbuild" during development while testing them on a spine compiled in "opt" that keeps running in the background.
This happens when your CPU is not powerful enough to run the simulator in real-time along with your agent and spine. You can call Spine::simulate
with nb_substeps = 1
instead of Spine::run
, which will result in the correct simulation time from the agent's point of view but make the simulation slower than real-time from your point of view.
Make sure you configure CPU isolation and set the scaling governor to performance
for real-time performance on a Raspberry Pi.
Interface description languages like Protocol Buffers are strongly typed: they formally specify a data exchange format that has to be written down and maintained, but brings benefits like versioning or breaking-change detection. Vulp, on the other hand, follows a weakly-typed, self-describing approach that is better suited to prototyping with rapidly-changing APIs: the spec is in the code. If an agent and spine communicate with incompatible/incomplete actions/observations, execution will break, begging for developers to fix it.
Vulp is designed for prototyping: it strives to eliminate intermediaries when it can, and keep a low barrier of entry. Python bindings bring the benefits of typing and are a good choice in production contexts, but like interface description languages, they also add overhead in terms of developer training, bookkeeping code and compilation time. Vulp rather goes for a crash-early approach: fast changes, fast blunders (interface errors raise exceptions that end execution), fast fixes (know immediately when an error was introduced).
That is not possible. One of the core assumptions in Vulp is that the agent and the spine are two respective processes communicating via one single shared-memory area. In this Vulp differs from e.g. ROS, which is multi-process by design. This design choice is discussed in #55.
Vulp means "fox" in Romansh, a language spoken in the Swiss canton of the Grisons. Foxes are arguably quite reliable in their reaction times 🦊