Vulp provides a standard action-observation loop to control actuators/simulators from Python. It looks like this:
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.
No installation required on Linux:
git clone https://github.com/tasts-robots/upkie_locomotion.git
cd upkie_locomotion
./tools/bazelisk run -c opt //agents/blue_balancer:bullet
Connect a USB controller to move the robot around. 🎮
The syntax is the same to deploy to the Raspberry Pi of a real robot like Upkie.
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.
- Run the same Python code on simulated and real robots
- Interfaces with to the mjbots pi3hat and mjbots actuators
- Interfaces with to the Bullet simulator
- Observer pipeline to extend observations
- 🏗️ Controller pipeline to extend actions
- Soft real-time: spine-agent loop interactions are predictable and repeatable
- Unit tested, and not only with end-to-end tests
- Low frequency: Vulp is designed for tasks that run in the 1–400 Hz range (like balancing bipeds or quadrupeds)
- Soft, not hard real-time guarantee: the code is empirically reliable by a large margin, that's it
- Weakly-typed IPC: typing is used within agents and spines, but the interface between them is only checked at runtime
If any of the non-features is a no-go to you, you may also want to check out these existing alternatives:
- mc_rtc - C++ real-time control framework from which Vulp inherited, among others, the idea of running the same code on simulated and real robots. The choice of a weakly-typed dictionary-based IPC was also inspired by mc_rtc's data store. C++ controllers are bigger cathedrals to build but they can run at higher frequencies.
- robot_interfaces - Similar IPC between non-realtime Python and real-time C++ processes. The main difference lies in the use of Python bindings and action/observation types (more overhead, more safeguards) where Vulp goes structureless (faster changes, faster blunders). Also, robot_interfaces enforces process synchronization with a time-series API while in Vulp this is up to the agent (most agents act greedily on the latest observation).
- ros2_control - A C++ framework for real-time control using ROS2 (still a work in progress). Its barrier of entry is steeper than the other alternatives, making it a fit for production rather than prototyping, as it aims for compatibility with other ROS frameworks like MoveIt. A Vulp C++ spine is equivalent to a ROS
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 bindings directly:
- moteus - Python bindings for moteus brushless controllers also run well up to 200 Hz.
- odri_control_interface - interface to control robots built with the ODRI Master Board.
Using control bindings directly is a simpler alternative if you don't need the action-observation loop and simulation/real-robot switch from Vulp.
Check out upkie_locomotion
to get started with a simulation and real-robot spines, custom observers and locomotion agents.
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_locomotion
) 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.
I have a Bullet simulation where the robot balances fine, but the agent repeatedly warns it "Skipped X clock cycles". What could be causing this?
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.
Why use dictionaries rather than an interface description language like Protocol Buffers?
Interface description languages formally specify an API, which costs some overhead to write down and maintain, but can bring versioning, breaking change detection and other benefits, especially when the API is stable. Vulp, on the other hand, is designed for prototyping with rapidly-changing APIs: the spec is in the code. If an agent and spine communicate with incompatible/incomplete actions/observations, execution will immediately 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).
Vulp means "fox" in Romansh, a language spoken in the Swiss canton of the Grisons. Foxes are arguably quite reliable in their reaction times 🦊