Import upstream snapshot 57d13ead53206a6bf0db3d66f86506ae8c2ce01a

Upstream: https://github.com/pollen-robotics/bam
Upstream-Commit: 57d13ead53206a6bf0db3d66f86506ae8c2ce01a
Upstream-Branch: mjlab_frictionloss
This commit is contained in:
Upstream Snapshot
2026-08-28 15:42:08 +08:00
commit 8be13016bc
131 changed files with 12114 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Minimal makefile for Sphinx documentation
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
VENV ?= .venv
SPHINXBUILD ?= $(if $(wildcard $(VENV)/bin/sphinx-build),$(VENV)/bin/sphinx-build,sphinx-build)
SOURCEDIR = .
BUILDDIR = _build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile clean api
clean:
rm -rf _build
# Rebuild API reference from scratch (forces autoapi to re-scan bam/)
api:
rm -rf autoapi/
@$(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+103
View File
@@ -0,0 +1,103 @@
/* Minimal custom style for BAM docs */
.bd-page-width {
max-width: 92rem;
}
h1, h2 {
letter-spacing: 0.01em;
}
/* Add breathing room above and below content images */
.bd-article img {
margin-top: 1rem;
margin-bottom: 2rem;
}
/* Center the left (trajectory name) column of the trajectories table */
.traj-table td:first-child,
.traj-table th:first-child {
text-align: center;
vertical-align: middle;
}
video {
width: 100%;
max-width:700px;
display: block;
margin-left: auto;
margin-right: auto;
}
/* Getting started cards */
.bam-getting-started {
margin-top: 1.5rem;
margin-bottom: 1rem;
}
.bam-card {
border-radius: 0.6rem;
transition: transform 0.15s ease, box-shadow 0.15s ease, border-color 0.15s ease;
}
.bam-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
border-color: var(--pst-color-primary);
}
.bam-card .sd-card-title {
font-size: 1.15rem;
}
/* Float the card footer link to the right */
.bam-card .sd-card-footer {
text-align: right;
}
/* "Use in simulation" buttons on the identified-actuators table */
.actuators-table td {
vertical-align: middle;
}
.motor-btn {
display: block;
width: 100%;
margin: 0.25rem 0;
white-space: nowrap;
}
/* Compact inline warning that fits inside a table cell */
.mini-warning {
margin: 0.5rem 0 0;
font-size: 0.8em;
line-height: 1.35;
color: var(--pst-color-warning, #966600);
}
.mini-warning code {
font-size: 0.9em;
}
/* Banner shown on usage pages indicating the motor the examples target */
.motor-banner {
display: flex;
align-items: center;
gap: 1.25rem;
margin-bottom: 1.5rem;
}
.motor-banner-img {
flex: 0 0 auto;
width: 90px;
height: auto;
margin: 0;
border-radius: 0.4rem;
}
.motor-banner-text {
flex: 1 1 auto;
}
.motor-banner-text p:last-child {
margin-bottom: 0;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 MiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

+103
View File
@@ -0,0 +1,103 @@
// Fill the {actuator} placeholder in the usage code examples with the motor
// chosen via the ?motor=<name> query parameter (set by the buttons on the
// "Identified actuators" page). When no motor is given, xl330 is used as the
// default. Only whitelisted motor names are ever inserted.
(function () {
"use strict";
// name -> human-readable label
var MOTORS = {
mx64: "MX-64",
mx106: "MX-106",
xl320: "XL-320",
xl330: "XL-330",
feetech_sts3215_7_4V: "STS3215 (7.4V)",
};
var DEFAULT_MOTOR = "xl330";
var PLACEHOLDER = "{actuator}";
function selectedMotor() {
var value = new URLSearchParams(window.location.search).get("motor");
if (value && Object.prototype.hasOwnProperty.call(MOTORS, value)) {
return value;
}
return DEFAULT_MOTOR;
}
function pageHasPlaceholder() {
var blocks = document.querySelectorAll("div.highlight pre");
for (var i = 0; i < blocks.length; i++) {
if (blocks[i].textContent.indexOf(PLACEHOLDER) !== -1) return true;
}
return false;
}
function updateCodeBlocks(motor) {
// The placeholder is a contiguous token inside a string literal, so a plain
// substring replace on innerHTML is enough (motor is whitelisted).
document.querySelectorAll("div.highlight pre").forEach(function (pre) {
if (pre.textContent.indexOf(PLACEHOLDER) === -1) return;
pre.innerHTML = pre.innerHTML.split(PLACEHOLDER).join(motor);
});
}
// Derive the "_static/" URL prefix from an existing asset link, so the motor
// image resolves correctly regardless of the current page's depth.
function staticBase() {
var link = document.querySelector('link[href*="_static/"]');
if (link) {
var href = link.getAttribute("href");
var idx = href.indexOf("_static/");
if (idx !== -1) return href.slice(0, idx + "_static/".length);
}
return "_static/";
}
function showBanner(motor) {
var article =
document.querySelector("article") ||
document.querySelector("main") ||
document.body;
var heading = article.querySelector("h1");
var img = staticBase() + "actuator_" + motor + ".png";
var banner = document.createElement("div");
banner.className = "admonition tip motor-banner";
banner.innerHTML =
'<img class="motor-banner-img" src="' +
img +
'" alt="' +
MOTORS[motor] +
'">' +
'<div class="motor-banner-text">' +
'<p class="admonition-title">Motor selected</p>' +
"<p>Examples on this page are shown for " +
'<code class="docutils literal notranslate"><span class="pre">' +
motor +
"</span></code> (" +
MOTORS[motor] +
"). " +
'<a href="actuators.html">Choose a different motor</a>.</p>' +
"</div>";
if (heading && heading.parentNode) {
heading.parentNode.insertBefore(banner, heading.nextSibling);
} else {
article.insertBefore(banner, article.firstChild);
}
}
function apply() {
// Only act on pages that actually carry the {actuator} placeholder.
if (!pageHasPlaceholder()) return;
var motor = selectedMotor();
updateCodeBlocks(motor);
showBanner(motor);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", apply);
} else {
apply();
}
})();
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 113 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 175 KiB

+7
View File
@@ -0,0 +1,7 @@
API Reference
=============
.. toctree::
:maxdepth: 2
../autoapi/bam/index
+64
View File
@@ -0,0 +1,64 @@
import os
import sys
project = "BAM"
author = "Marc Duclusaud & Grégoire Passault"
copyright = "2026, Marc Duclusaud & Grégoire Passault"
extensions = [
"autoapi.extension",
"sphinx.ext.napoleon",
"sphinx.ext.mathjax",
"sphinx.ext.todo",
"sphinx_copybutton",
"sphinxcontrib.video",
"sphinx_design",
]
templates_path = ["_templates"]
exclude_patterns = ["_build", ".venv", "Thumbs.db", ".DS_Store"]
sys.path.insert(0, os.path.abspath(".."))
# sphinx-autoapi — scans bam/ via AST, no runtime imports needed
autoapi_type = "python"
autoapi_dirs = ["../bam"]
autoapi_root = "autoapi"
autoapi_ignore = ["*/drive_backdrive.py", "*/plot.py", "*/jitter.py", "*/mae.py", "*/message.py", "*/process.py"]
autoapi_add_toctree_entry = False
autoapi_options = [
"members",
"undoc-members",
"show-inheritance",
"show-module-summary",
"imported-members",
]
autoapi_python_class_content = "both"
autoapi_member_order = "bysource"
napoleon_google_docstring = True
napoleon_numpy_docstring = False
todo_include_todos = True
html_theme = "pydata_sphinx_theme"
pygments_style = "friendly"
pygments_dark_style = "monokai"
html_static_path = ["_static"]
html_css_files = ["bam.css"]
html_js_files = ["motor_examples.js"]
html_logo = "_static/BAM_logo.png"
html_favicon = "_static/favicon.ico"
html_theme_options = {
"show_nav_level": 2,
"show_toc_level": 2,
"navigation_with_keys": True,
"icon_links": [
{
"name": "GitHub",
"url": "https://github.com/Rhoban/bam",
"icon": "fa-brands fa-square-github",
"type": "fontawesome",
}
],
}
+241
View File
@@ -0,0 +1,241 @@
Data Acquisition
================
Recording consists of running the actuator through a set of predefined
trajectories while logging position, velocity, and control signals. The
trajectories are played with different P-gain values and with each
mass/length combination. The resulting data is then processed to a fixed
timestep and stored in a structured format for later use in the identification
pipeline.
Installation
------------
First, you need to clone the `BAM repository <https://github.com/Rhoban/bam>`_.
Then, install the extra dependencies for the identification pipeline using
``uv`` (`installation instructions <https://docs.astral.sh/uv/getting-started/installation/>`_):
.. code-block:: text
uv sync --extra identification
Trajectories
----------------------
The trajectories are designed to excite different friction regimes.
Each trajectory runs for 6 seconds.
.. list-table::
:header-rows: 1
:widths: 20 80
:class: traj-table
* - Name
- Description
* - ``sin_time_square``
- :math:`\sin(t^2)` profile — progressively faster oscillations, good
general-purpose trajectory that covers a wide velocity range.
|traj_sin_time_square|
* - ``lift_and_drop``
- Cubic move to −π/2 over 2 s, then torque disabled — the arm falls
under gravity. Disabling the torque can allow to dissociate viscous friction
from the back-EMF induced damping.
|traj_lift_and_drop|
* - ``up_and_down``
- Cubic path 0 → π/2 → 0.8·π/2 — slower motion, emphasizes static
friction and load-dependent effects.
|traj_up_and_down|
* - ``sin_sin``
- :math:`\sin(t)\cdot\pi/2 + \sin(5t)\cdot 0.5\cdot\sin(2t)` — rich
multi-frequency content.
|traj_sin_sin|
.. |traj_sin_time_square| image:: /_static/traj_sin_time_square.png
:width: 100%
:alt: sin_time_square trajectory
.. |traj_lift_and_drop| image:: /_static/traj_lift_and_drop.png
:width: 100%
:alt: lift_and_drop trajectory
.. |traj_up_and_down| image:: /_static/traj_up_and_down.png
:width: 100%
:alt: up_and_down trajectory
.. |traj_sin_sin| image:: /_static/traj_sin_sin.png
:width: 100%
:alt: sin_sin trajectory
Recording
---------
BAM already ships with support for several actuators (the ones whose models
are provided in the library), and is designed to be extended with new ones.
Each manufacturer is implemented in its own package under
``bam/<manufacturer>/``, exposing the same small interface. To record your own
actuator, you therefore either:
- **extend an existing manufacturer package** if your motor's brand is already
supported — simply add your actuator class (for example a new Dynamixel
model in ``bam/dynamixel/``); or
- **create a new** ``bam/<manufacturer>/`` **package** if your manufacturer is
not supported yet.
Each manufacturer package is made of three modules:
- ``actuator.py`` — the only hardware-specific part. It handles the
communication with the motor: reading position, speed and load, and sending
position or current commands. Your actuator class subclasses the base
classes provided in :mod:`bam.actuator`.
- ``record.py`` — plays the trajectories on the motor and logs the resulting
data to JSON. It adapts the generic trajectories to the motor when needed
(for instance, scaling the velocity to stay within the actuator's limits).
- ``all_record.py`` — a convenience script that runs a full recording session
for a given mass/arm configuration, automatically sweeping over all the
P-gain values.
When writing your own package, the existing ``bam/dynamixel/`` implementation
is a good reference to draw inspiration from.
Once these modules are written, recording should be done using the following command:
.. code-block:: text
uv run python -m bam.<manufacturer>.all_record \
--port /dev/ttyUSB0 \
--motor motor_name \
--mass 0.5 \
--arm-mass 0.02 \
--length 0.15 \
--vin 7.5 \
--logdir data_raw
with each length/mass combination. Don't forget to update the mass, length, and arm mass
parameters for each recording.
.. warning::
The P-gain values used in the ``<manufacturer>/record.py`` file are
manufacturer-specific and must be adapted to your motor based on the
manufacturer's specifications: a gain that is meaningful for one firmware
may be far too high or too low for another.
To determine the appropriate P-gain values for your actuator, you can
take the default P-gain value ``kp`` given by the manufacturer and test
[kp/6, kp/4, kp/2, kp].
Raw data format
---------------
Each recording produces one JSON file:
.. code-block:: json
{
"mass": 0.5,
"arm_mass": 0.02,
"length": 0.15,
"kp": 50,
"vin": 7.5,
"motor": "motor_name",
"trajectory": "sin_time_square",
"entries": [
{
"timestamp": 0.0077,
"position": 0.0015,
"speed": 0.024,
"load": 0.0,
"input_volts": 7.5,
"goal_position": 0.0,
"torque_enable": true
},
...
]
}
Entries are logged at the firmware's native rate, which is not necessarily
constant. The processing step resamples them to a fixed timestep.
Checking jitter
---------------
You can use the following script:
.. code-block:: bash
uv run python -m bam.jitter --logdir data_raw/
To visualize the jitter histogram.
Processing
----------
Resample raw logs to a constant timestep before fitting:
.. code-block:: text
uv run python -m bam.process \
--raw data_raw \
--logdir data_processed \
--dt 0.005
``--dt`` is the target timestep in seconds. The
script linearly interpolates between consecutive entries and writes one
processed JSON per raw file into ``data_processed/``.
Plotting
--------
You can plot the processed data using the following command:
.. code-block:: bash
uv run python -m bam.plot \
--actuator xl330 \
--logdir data_processed
Example: Dynamixel XL-330
-------------------------
The Dynamixel XL-330 is supported through the ``bam/dynamixel/``
package. The physical test bench used for this motor is shown in
:doc:`setup`.
To record a full session for a 0.567 kg weight at the tip of a 0.17 m
arm weighing 0.016 kg, the following command is used:
.. code-block:: text
uv run python -m bam.dynamixel.all_record \
--port /dev/ttyUSB0 \
--motor xl330 \
--mass 0.567 \
--arm-mass 0.016 \
--length 0.17 \
--vin 7.5 \
--logdir data_raw
For the XL-330, ``all_record`` automatically sweeps the five P-gain values
``[50, 100, 200, 300, 400]`` over the four trajectories, producing 20
recordings in ``data_raw/``. By repeating the command for each mass/length
combination presented in :doc:`setup`, a complete dataset of 240 6s recordings is obtained.
The video below shows the four trajectories being played on the XL-330 for a
single P-gain value:
.. raw:: html
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden; max-width: 100%; margin: 1rem 0 2rem;">
<iframe src="https://www.youtube.com/embed/sWn3lr7Sf4I"
title="XL-330 trajectory acquisition"
style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: 0;"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
</div>
Once all sessions are recorded, the raw data are then processed into a fixed timestep as shown
above.
+95
View File
@@ -0,0 +1,95 @@
Modeling the actuator
=====================
Before you start getting some data, you need to model your actuator in BAM software. Modeling an actuator consist
in subclassing BAM's :class:`~bam.actuator.Actuator` class and implementing the required methods. The following
questions need to be addressed:
* How is the servomotor turning the state (encoder position, velocity) into **torque** ?
* What **control law** is in play ?
* What **limits** or **smoothing** is applied in the servo firmware ?
* What important **values** need to be identified to model the actuator ?
* In particular, you might want to have some initial guess ranges for those values
Fortunately, the answers to those questions are often the same. That is why BAM already provides few
subclasses of :class:`~bam.actuator.Actuator`:
* :class:`~bam.actuator.VoltageControlledActuator` for actuators that are controlled by a voltage command
* :class:`~bam.actuator.CurrentControlledActuator` for actuators that are controlled by a current command
Voltage controlled actuator
---------------------------
If the command produced in firmware is a voltage, then you should subclass :class:`~bam.actuator.VoltageControlledActuator`. Find examples of such subclassing in `Dynamixel's actuator.py <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`_.
Here are the constructor parameters:
* ``vin``: the supply voltage for the actuator
* ``kp``: the proportional gain of the firmware control law, we recommend keeping it in *firmware unit*, meaning that you can use the exact same value as the one documented by the vendor
* ``error_gain``: this gain should be such that ``u = error_gain * kp * (target - position)`` is the voltage command sent to the motor. It is a constant that correct the firmware unit into a voltage unit
* ``max_pwm``: the maximum PWM value (between ``0`` and ``1``) that the firmware can set the PWM to. If you don't know, you can set it to ``1``.
* ``max_current``: if the firmware has a current limit, you can set it here. If you don't know, you can set it to ``None``.
And here are the typical model parameters you'll want to identify:
* :math:`k_t`: the torque constant of the motor, in :math:`N.m/A`
* :math:`R`: the resistance of the motor, in :math:`\Omega`
* :math:`I`: the apparent inertia of the motor, in :math:`kg.m^2`
See below for initial guesses and ranges for those parameters.
Current controlled actuator
---------------------------
If the command produced in firmware is a current, then you should subclass :class:`~bam.actuator.CurrentControlledActuator`. Find examples of such subclassing in `Dynamixel's actuator.py <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`_.
Here are the constructor parameters:
* ``vin``: the supply voltage for the actuator
* ``kp``: the proportional gain of the firmware control law, we recommend keeping it in *firmware unit*, meaning that you can use the exact same value as the one documented by the vendor
* ``error_gain``: this gain should be such that ``i = error_gain * kp * (target - position)`` is the current command sent to the motor.
And here are the typical model parameters you'll want to identify:
* :math:`k_t`: the torque constant of the motor, in :math:`N.m/A`
* :math:`R`: the resistance of the motor, in :math:`\Omega`
* :math:`I`: the apparent inertia of the motor, in :math:`kg.m^2`
* :math:`I_{max}`: the maximum current that the firmware can send to the motor, in :math:`A`
Initial guesses and ranges
---------------------------
When declaring a model, you need to provide initial guesses and ranges for the parameters. For example:
.. code-block:: python
# Torque constant [Nm/A] or [V/(rad/s)]
self.model.kt = Parameter(0.7, 0.25, 1.5)
This tells BAM that the initial guess for :math:`k_t` is ``0.7``, and that it should be searched in the range ``[0.25, 1.5]``.
Torque constant :math:`k_t`
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Torque constant :math:`k_t` maps current in :math:`A` to torque in :math:`N.m`. It turns out that the same constant
also maps the motor velocity in :math:`rad/s` to back-EMF voltage in :math:`V`.
Thus, to estimate :math:`k_t`, you can apply a known voltage to the motor and measure its free speed :math:`\omega`. The guess for :math:`k_t` is then given by :math:`k_t = \frac{V}{\omega}`.
Don't hesitate to take multiple samples and use a method like least square to estimate the torque constant more accurately.
Resistance :math:`R`
~~~~~~~~~~~~~~~~~~~~
Motor resistance is usually small value, that you will not get accurately approximated with measuring tool like
multimeter. The best way to estimate it is to measure the current draw of the motor when applying a known voltage command, while keeping the motor blocked. Then, you can use Ohm's law to estimate the resistance :math:`R = \frac{V}{I}`.
Apparent inertia :math:`I`
~~~~~~~~~~~~~~~~~~~~~~~~~~
Apparent inertia is the rotor inertia of the motor, which is multiplied by the square of the gear ratio. While the reduction ratio is often known, it is not easy to guess the value of the rotor inertia.
This value can typically be adjusted across multiple BAM fits, monitor if the value saturates either up or low during
identification and ajust the range accordingly.
+73
View File
@@ -0,0 +1,73 @@
Contributing a Model
====================
If you have identified parameters for a motor that is not yet in the BAM
library, sharing them benefits the whole community. This page explains how
to format and submit your contribution.
What to include
---------------
A contribution consists of:
- One JSON parameter file per model variant (at minimum M6; ideally M1–M6).
- Update in this documentation, adding your actuator to the :doc:`usage/actuators` page.
- Ideally, provide raw data so that the fitting is reproducible
Parameter file format
---------------------
The JSON file produced by ``uv run python -m bam.fit`` is the expected format:
.. code-block:: json
{
"model": "m6",
"actuator": "my_motor",
"kt": 1.23,
"R": 4.56,
"armature": 0.01,
"friction_base": 0.05,
"friction_viscous": 0.02,
"friction_stribeck": 0.08,
"dtheta_stribeck": 1.5,
"alpha": 1.2,
"load_friction_motor": 0.15,
"load_friction_external": 0.03,
"load_friction_motor_stribeck": 0.10,
"load_friction_external_stribeck": 0.04,
"load_friction_motor_quad": 0.005,
"load_friction_external_quad": 0.001,
"q_offset": 0.0
}
The file is generated automatically at the end of a fitting run. No manual
editing is required.
Quality checklist
-----------------
Before submitting, verify:
- The identified parameters reproduce recorded trajectories with a reasonable
MAE (use ``uv run python -m bam.plot --sim`` to inspect the fit visually).
- The fit was validated on a hold-out set (``--validation_kp``) to check for
overfitting.
- All six models (M1–M6) have been fitted so users can choose the complexity
level appropriate for their use case.
Submitting
----------
Open a pull request on the `BAM GitHub repository <https://github.com/Rhoban/bam>`_
and place your parameter files under:
.. code-block:: text
bam/params/<motor_name>/m1.json
bam/params/<motor_name>/m2.json
...
bam/params/<motor_name>/m6.json
Include a short description of the motor and the identification conditions in
the pull request body.
+167
View File
@@ -0,0 +1,167 @@
Fitting
=======
The fitting step optimizes the friction model parameters so that the BAM
simulator reproduces the recorded trajectories as closely as possible. The
objective is the mean absolute error (MAE) between simulated and measured
joint positions, averaged across all logs.
Running the fit
---------------
.. code-block:: text
uv run python -m bam.fit \
--actuator xl330 \
--model m6 \
--logdir data_processed \
--output params/xl330/m6.json
``--actuator`` must match the motor name used during recording.
``--model`` selects the friction model variant (``m1`` through ``m6``).
The optimizer writes ``params.json`` every few seconds as it runs, so
progress can be monitored by inspecting the output file or running
``uv run python -m bam.plot`` in parallel.
Optimization options
--------------------
.. list-table::
:header-rows: 1
:widths: 25 15 60
* - Argument
- Default
- Description
* - ``--method``
- ``cmaes``
- Optimization algorithm: ``cmaes`` (CMA-ES with BIPOP restart),
``random``, or ``nsgaii``.
* - ``--trials``
- 100 000
- Number of evaluations. Increase for better convergence on complex
models (M5, M6).
* - ``--workers``
- 1
- Number of parallel workers. Uses a shared SQLite study database when
greater than 1.
* - ``--load-study``
- —
- Path to an existing Optuna study to resume optimization.
* - ``--reset_period``
- —
- Re-synchronize the simulator state to the log at this interval
[seconds]. Useful when accumulated error destabilizes long rollouts.
Validation split
----------------
To detect overfitting, hold out the logs recorded at one P-gain value and
use them as a validation set:
.. code-block:: text
uv run python -m bam.fit \
--actuator xl330 \
--model m6 \
--logdir data_processed \
--validation_kp 8 \
--output params/xl330/m6.json
Logs recorded with ``--kp 8`` are excluded from training and evaluated
separately. The best validation MAE is reported alongside the training score.
Fitting all models
------------------
It is recommended to fit all six models and compare their validation error:
.. code-block:: text
for model in m1 m2 m3 m4 m5 m6; do
uv run python -m bam.fit \
--actuator xl330 \
--model $model \
--logdir data_processed \
--validation_kp 8 \
--output params/xl330/$model.json \
--trials 100000
done
Simpler models (M1, M2) train faster; richer models (M5, M6) may need more
trials to converge but can capture directional and load-dependent effects
that simpler models miss. The model with the best validation MAE and
acceptable parameter count is typically the right choice.
Output file
-----------
The optimizer writes a JSON file containing the identified parameters and
metadata:
.. code-block:: json
{
"model": "m6",
"actuator": "xl330",
"kt": 2.21,
"R": 2.03,
"armature": 0.026,
"friction_base": 1.0e-05,
"friction_viscous": 0.051,
"friction_stribeck": 0.122,
"dtheta_stribeck": 1.75,
"alpha": 1.14,
"load_friction_motor": 0.198,
"load_friction_external": 0.022,
"load_friction_motor_stribeck": 0.199,
"load_friction_external_stribeck": 0.087,
"load_friction_motor_quad": 0.010,
"load_friction_external_quad": 7.3e-05,
"q_offset": 0.0
}
The file can be passed directly to :func:`bam.model.load_model` or used as
``json_path`` in :class:`bam.mjlab.BamActuatorCfg`.
Evaluating and visualizing results
-----------------------------------
To evaluate a parameter file on the recorded logs:
.. code-block:: text
uv run python -m bam.fit \
--actuator xl330 \
--model m6 \
--logdir data_processed \
--eval \
--output params/xl330/m6.json
To plot measured versus simulated trajectories:
.. code-block:: text
uv run python -m bam.plot \
--actuator xl330 \
--logdir data_processed \
--sim \
--params params/xl330/m6.json
Several ``--params`` files can be given to overlay multiple models on the
same plot, which is useful for comparing M1 through M6 side by side.
Weights & Biases logging
------------------------
Pass ``--wandb`` to stream training and validation metrics to a W&B project:
.. code-block:: text
uv run python -m bam.fit \
--actuator xl330 \
--model m6 \
--logdir data_processed \
--wandb \
--output params/xl330/m6.json
+73
View File
@@ -0,0 +1,73 @@
Identification
==============
This section walks through the end-to-end identification pipeline: building
the test bench, recording trajectories, and fitting a friction model.
.. grid:: 1 2 2 2
:gutter: 3
:class-container: bam-getting-started
.. grid-item-card:: 🔩 Hardware setup
:link: setup
:link-type: doc
:class-card: bam-card
Build the pendulum test bench with variable loads and lengths to collect
a rich set of trajectories.
+++
:doc:`Hardware setup → <setup>`
.. grid-item-card:: 🧩 Modeling the actuator
:link: actuator_modeling
:link-type: doc
:class-card: bam-card
Subclass BAM's :class:`~bam.actuator.Actuator` and implement the methods
that describe your motor.
+++
:doc:`Actuator modeling → <actuator_modeling>`
.. grid-item-card:: 🎬 Data acquisition
:link: acquisition
:link-type: doc
:class-card: bam-card
Run the actuator through predefined trajectories while logging position,
velocity, and control signals.
+++
:doc:`Data acquisition → <acquisition>`
.. grid-item-card:: 📉 Fitting
:link: fitting
:link-type: doc
:class-card: bam-card
Optimize the friction model parameters so the simulator reproduces the
recorded trajectories.
+++
:doc:`Fitting → <fitting>`
.. grid-item-card:: 🤝 Contributing a model
:link: contributing
:link-type: doc
:class-card: bam-card
Identified a new motor? Share your parameters with the community and add
them to the BAM library.
+++
:doc:`Contributing → <contributing>`
.. toctree::
:hidden:
setup
actuator_modeling
acquisition
fitting
contributing
+67
View File
@@ -0,0 +1,67 @@
Hardware Setup
==============
BAM's identification pipeline requires a pendulum test bench with variable
loads and lengths to increase the variety of the collected logs. The pendulum
is attached directly to the actuator's output shaft, optionally using a
counter-shaft when the actuator supports it.
To present the identification process, the Dynamixel XL-330 motor is used as
a case study. The instructions are similar for other actuators, adjusting only
for the mass and length parameters.
Requirements
------------
The hardware required for the identification process is as follows:
- **A set of rigid arms of varying lengths**.
The mass of the arms should be negligible compared to the attached loads.
Therefore, it is recommended to use 3D-printed or laser-cut wooden arms for
smaller motors handling light loads, and to reserve metal arms for more
powerful motors.
- **A set of weights compatible with the arms**.
The masses must be heavy enough to generate a wide range of load torques.
- **A mounting bracket for the actuator**.
The setup must ensure that the actuator remains firmly secured during data
logging, despite the fast movements of the weights at the end of the arm. The
arm-weight assemblies must have sufficient clearance to oscillate between
+/- 90° relative to the vertical position.
- **A communication interface for the actuator**.
Once the hardware is gathered, you must record the mass of the weights as well
as the length and mass of the arms. These parameters are mandatory for the identification process.
Example: Dynamixel XL-330
-------------------------
Here is an example of a test bench for the Dynamixel XL-330 actuator.
The pendulum arms are 3D printed; you can refer to the 3D model for inspiration
`here <https://cad.onshape.com/documents/c132b33797dc72aa58be8a7c/v/d9921c19039fee61b5e51bba/e/8e6b266057c61625732fd9b0?renderMode=0&uiState=6a447ab873e2fce629279db5>`_.
.. image:: /_static/hardware_1.png
:width: 80%
:align: center
:alt: 3D-printed pendulum test bench for the Dynamixel XL-330
The interface with the actuator is established using a U2D2 kit.
.. image:: /_static/hardware_2.png
:width: 80%
:align: center
:alt: U2D2 communication interface
The complete setup is shown below. The pendulum is attached to the
actuator's output shaft, and the masses are fixed to the end of the
pendulum arm.
.. image:: /_static/hardware_3.png
:width: 80%
:align: center
:alt: Complete Dynamixel XL-330 test bench setup
+129
View File
@@ -0,0 +1,129 @@
Better Actuator Models (BAM)
============================
.. grid:: 1 2 2 2
:gutter: 2
.. grid-item::
.. video:: https://github.com/Rhoban/bam_media/raw/refs/heads/main/videos/m1_vs_m6.mp4
:autoplay:
:muted:
:loop:
.. grid-item::
.. video:: https://github.com/Rhoban/bam_media/raw/refs/heads/main/videos/sin_sin.mp4
:autoplay:
:muted:
:loop:
.. admonition:: What is it?
BAM (for Better Actuator Models) is a library to **identify** and **use** servo-actuator models in simulation
engines. In particular, it provides **extended friction models**, which are more accurate than the default friction
models used in most simulators (e.g., MuJoCo).
The animation above is a face-to-face comparison of the :doc:`default MuJoCo friction model (M1) <theory/models>` and an :doc:`extended friction model (M6) <theory/models>` identified for Dynamixel MX-64 and MX-106 servo-actuators lifting a load.
Features
--------
- :doc:`Pre-identified friction models <usage/actuators>` for common servo-actuators (e.g., XL320, XL330, MX64, MX106...)
- **API for direct integration of friction models** into :doc:`MuJoCo CPU <usage/mujoco_cpu>` and :doc:`mjlab (MuJoCo Warp) <usage/mjlab_gpu>`
- :doc:`Extended friction models <theory/index>`, including Stribeck effect, load-dependance
- :doc:`Identification process <identification/index>` for servo-actuator with friction models
Getting started
---------------
Pick the path that matches where you are:
.. grid:: 1 1 3 3
:gutter: 3
:class-container: bam-getting-started
.. grid-item-card:: 🔌 Use a bundled model
:link: usage/index
:link-type: doc
:class-card: bam-card
Your motor is already in the :doc:`library <usage/actuators>`
(``xl320``, ``xl330``, ``mx64``, ``mx106``, ...).
Load its parameters and plug them into your MuJoCo or mjlab simulation
right away.
+++
:doc:`Usage guide → <usage/index>`
.. grid-item-card:: 🔧 Identify a new motor
:link: identification/index
:link-type: doc
:class-card: bam-card
Your motor is not in the library yet.
Build a simple pendulum test bench, record trajectories under varying
load, and run the identification pipeline to fit a friction model.
+++
:doc:`Identification process → <identification/index>`
.. grid-item-card:: 📐 Understand the theory
:link: theory/index
:link-type: doc
:class-card: bam-card
You want to understand the modeling approach.
Dive into the pendulum dynamics, the friction-budget formulation, and
the six model variants M1–M6.
+++
:doc:`Theory → <theory/index>`
Reference paper
---------------
This library implements the modeling and identification approach introduced
in the paper `"Extended Friction Models for the Physics Simulation of Servo
Actuators" <https://arxiv.org/pdf/2410.08650>`_ by Marc Duclusaud, Gregoire Passault, Vincent Padois, and Olivier
Ly .
If you use BAM or the friction models provided in this library for your
scientific work, please cite the publication:
.. code-block:: bibtex
@inproceedings{duclusaud2025extended,
title={Extended Friction Models for the Physics Simulation of Servo Actuators},
author={Duclusaud, Marc and Passault, Gr{\'e}goire and Padois, Vincent and Ly, Olivier},
booktitle={2025 IEEE International Conference on Robotics and Automation (ICRA)},
pages={12091--12097},
year={2025},
organization={IEEE}
}
If you also wish to cite the software directly, you may use:
.. code-block:: bibtex
@software{BAM,
title = {{BAM: Better Actuator Models}},
author = {Duclusaud, Marc and Passault, Grégoire},
license = {Apache-2.0},
url = {https://github.com/Rhoban/bam},
version = {0.0.1},
year = {2024}
}
.. toctree::
:maxdepth: 2
:caption: Contents
usage/index
identification/index
theory/index
api/index
+7
View File
@@ -0,0 +1,7 @@
sphinx>=7.2,<9
pydata-sphinx-theme>=0.15
sphinx-copybutton>=0.5
sphinx-autobuild>=2024.4.16
sphinx-autoapi>=3.0
sphinxcontrib-video
sphinx-design>=0.6
+61
View File
@@ -0,0 +1,61 @@
DC Motor
========
In BAM, servomotor sources are modeled using DC motors dynamics. A DC motor turns current into torque, according
to the following equation:
.. math::
\tau = k_t i
where :math:`\tau` is the torque, :math:`k_t` is the torque constant, and :math:`i` is the current. When a motor
rotates at a velocity of :math:`\omega`, it generates a back electromotive voltage of :math:`k_t \omega`.
As a result, if you apply a voltage :math:`V` to the motor, the current is given by:
.. math::
i = \frac{V - k_t \omega}{R}
where :math:`R` is the motor resistance. Thus, the torque generated by a DC motor is given by:
.. math::
\tau = k_t \frac{V}{R} - k_t^2 \frac{\omega}{R}
Torque vs speed limit
---------------------
There are typically two limits in play in a DC motor:
* The maximum **voltage** you can apply, this is typically limited because of your battery that, for example,
can only provide 12V
* The maximum **current** you can apply, this is typically enforced by firmwares to avoid overheating the motor.
Let's first look at the voltage limit, using above equation, we get:
.. image:: ../_static/velocity_vs_torque.svg
:alt: Torque vs speed limit
:width: 400
:align: center
The feasible green area here is depicting the limit caused by **voltage** limit. Note that the torque you can apply actually depends on the current speed of the motor. At a given velocity :math:`\omega_0`, you will not even be
able to apply positive torque (this is the no-load speed of the motor). Conversely, at zero speed, there is a
maximum torque :math:`\tau_{stall}` you can apply (this is the stall torque of the motor).
It is in general a good idea to use voltages as high as possible. Firstly because it allows to use smaller wires,
but also because it can make the voltage-related limit less restrictive. However, many servo-actuators run with
low-voltage batteries and fall into that functioning regime.
The second limit is the **current** limit, that is typically enforced by the firmware of the motor. Since the torque
is proportional to current, this limit can be seen as a maximum torque limit :math:`\tau_{max}`:
.. image:: ../_static/velocity_vs_torque_custom_tau_max.svg
:alt: Torque vs speed limit
:width: 400
:align: center
Note that this is, however a *soft* limit. The firmware will *attempt* to limit the current, but it is not guaranteed that it will always be able to do so.
For example, if the motor rotates at a very high speed, the best strategy to provide the less torque is to provide
as much volt as possible. However, your voltage supply is limited, and you might have no choice that to provide a voltage that will generate a current above the limit.
+55
View File
@@ -0,0 +1,55 @@
Theory
======
This section covers the theoretical foundations behind BAM: the pendulum
bench dynamics, the friction-budget formulation used in simulation, and the
six friction model variants M1–M6.
It follows the modeling and identification approach introduced
in the paper `"Extended Friction Models for the Physics Simulation of Servo
Actuators" <https://arxiv.org/pdf/2410.08650>`_ by Marc Duclusaud, Gregoire Passault, Vincent Padois, and Olivier
Ly .
.. grid:: 1 2 2 2
:gutter: 3
:class-container: bam-getting-started
.. grid-item-card:: 📐 Theoretical framework
:link: theory
:link-type: doc
:class-card: bam-card
The pendulum test bench dynamics and the friction-budget formulation
used to apply BAM models in simulation.
+++
:doc:`Theoretical framework → <theory>`
.. grid-item-card:: 🧮 Friction models (M1–M6)
:link: models
:link-type: doc
:class-card: bam-card
The six friction models of increasing expressiveness, from Coulomb
friction to load-dependent and Stribeck effects.
+++
:doc:`Friction models → <models>`
.. grid-item-card:: ⚙️ DC motor
:link: dc_motor
:link-type: doc
:class-card: bam-card
How servomotor sources are modeled as DC motors, turning current into
torque.
+++
:doc:`DC motor → <dc_motor>`
.. toctree::
:hidden:
theory
models
dc_motor
+183
View File
@@ -0,0 +1,183 @@
Friction Models (M1-M6)
=======================
BAM supports six friction models of increasing expressiveness, denoted
:math:`\mathcal{M}_1` to :math:`\mathcal{M}_6`.
They are ordered from the simplest Coulomb-Viscous approximation to a richer
directional and quadratic formulation that better captures gearbox behavior.
- :math:`\mathcal{M}_1`: Coulomb-Viscous
- :math:`\mathcal{M}_2`: Stribeck
- :math:`\mathcal{M}_3`: Load-dependent
- :math:`\mathcal{M}_4`: Stribeck + load-dependent
- :math:`\mathcal{M}_5`: Directional load-dependent
- :math:`\mathcal{M}_6`: Quadratic directional variant
Notation
--------
In the equations below:
- :math:`\dot{\theta}` is joint velocity
- :math:`\tau_m` is motor torque
- :math:`\tau_e` is external/load torque
- :math:`\tau_{fm}` is the maximum resistive friction torque (friction budget)
The simulation then applies friction by clipping the stopping torque in
:math:`[-\tau_{fm},\tau_{fm}]`.
Drive/backdrive diagrams
------------------------
The drive/backdrive diagrams help to visualize the effect of friction. Let's for example consider the :math:`\mathcal{M}_6` model:
.. image:: ../_static/drive_backdrive_m6.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
Here:
- Below the blue line, the motor is driving the system
- Above the red line, the system is backdriving the motor
- On the black dashed line, motor torque and external torque exacttly cancel out
- In the middle, the friction budget is preventing motion in the system
- The lines with less opacity depicts what happens when the system is moving (1 rad/s per step). As you can notice, the faster the system is moving, the less friction appears here.
They can be obtained using ``bam.drive_backdrive`` command from the repository:
.. code-block:: bash
uv run -m bam.drive_backdrive --params bam/params/erob80_100/m1.json --max_torque 100
Model :math:`\mathcal{M}_1`: Coulomb-Viscous
--------------------------------------------
.. image:: ../_static/drive_backdrive_m1.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_1:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c
This is the baseline model used in most physics simulators.
It keeps only viscous damping and a constant Coulomb term.
Model :math:`\mathcal{M}_2`: Stribeck
-------------------------------------
.. image:: ../_static/drive_backdrive_m2.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_2:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c +
\exp\left(-\left|\frac{\dot{\theta}}{\dot{\theta}_s}\right|^{\alpha}\right)K_{cs}
This adds higher friction near zero speed and smooth transition to sliding.
It models the fact that static friction is usually stronger than sliding friction.
Model :math:`\mathcal{M}_3`: Load-dependent
-------------------------------------------
.. image:: ../_static/drive_backdrive_m3.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_3:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c + K_l|\tau_m - \tau_e|
This captures the increase of friction with transmitted gearbox load.
It is useful when the apparent resistance depends on how hard the transmission is loaded.
Model :math:`\mathcal{M}_4`: Stribeck + load-dependent
------------------------------------------------------
.. image:: ../_static/drive_backdrive_m4.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_4:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c + K_l|\tau_m-\tau_e|
+ \exp\left(-\left|\frac{\dot{\theta}}{\dot{\theta}_s}\right|^{\alpha}\right)
\left(K_{cs} + K_{ls}|\tau_m-\tau_e|\right)
This combines presliding dynamics with load dependence.
It adds Stribeck smoothing on top of a load-sensitive friction budget.
Model :math:`\mathcal{M}_5`: Directional load-dependent
-------------------------------------------------------
.. image:: ../_static/drive_backdrive_m5.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_5:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c + |K_m\tau_m - K_e\tau_e|
+ \exp\left(-\left|\frac{\dot{\theta}}{\dot{\theta}_s}\right|^{\alpha}\right)
\left(K_{cs} + |K_{ms}\tau_m - K_{es}\tau_e|\right)
This separates motor-side and external-side contributions, which helps model
directional efficiency/backdrivability asymmetry.
It is appropriate when the gearbox behaves differently depending on the torque direction.
Model :math:`\mathcal{M}_6`: Quadratic directional
--------------------------------------------------
.. image:: ../_static/drive_backdrive_m6.png
:alt: Drive/backdrive diagram
:width: 500
:align: center
.. math::
\mathcal{M}_6:\quad
\tau_{fm} = K_v|\dot{\theta}| + K_c + |K_m\tau_m - K_e\tau_e|
+ \exp\left(-\left|\frac{\dot{\theta}}{\dot{\theta}_s}\right|^{\alpha}\right)
\left(K_{cs} + |K_{ms}\tau_m - K_{es}\tau_e| + Q\right)
with piecewise quadratic contribution:
.. math::
Q =
\begin{cases}
K_{eq}\tau_e^2 & \text{if } |\tau_m| > |\tau_e| \\
K_{mq}\tau_m^2 & \text{otherwise}
\end{cases}
This is useful for actuators where harmonic-drive-like effects create nonlinear
load-friction coupling.
It extends the directional model with a quadratic load term.
Modeling hierarchy
------------------
The sequence :math:`\mathcal{M}_1 \rightarrow \mathcal{M}_6` reflects increasing
expressiveness and parameter count. In practice, BAM fits all candidates and
selects the best trade-off from validation error.
Implementation in BAM
---------------------
Model behaviors are implemented in :mod:`bam.model`, notably:
- :class:`bam.model.Model`
- :func:`bam.model.load_model`
- :func:`bam.model.load_model_from_dict`
+66
View File
@@ -0,0 +1,66 @@
Theoretical Framework
=====================
Pendulum test bench dynamics
----------------------------
For a single-axis pendulum test bench:
.. math::
\tau_m + \tau_e(\theta) + \tau_f = J\ddot{\theta}
where:
- :math:`\theta` is the joint position
- :math:`\tau_m` is the actuator torque
- :math:`\tau_e(\theta) = mgl\sin(\theta)` is the gravity torque
- :math:`\tau_f` is the friction torque
- :math:`J` is the apparent inertia (load + actuator side)
Friction as a torque budget
---------------------------
The Coulomb-Viscous form is typically written as a force opposed to velocity.
In BAM, friction is handled through a maximum available resistive torque
:math:`\tau_{fm}`.
For a discrete simulator with time step :math:`\Delta t`, stopping at next step
requires:
.. math::
\dot{\theta}_{k+1} = 0
which yields the stopping torque:
.. math::
\tau_{f,stop} = -\left(\frac{J}{\Delta t}\dot{\theta} + \tau_m + \tau_e\right)
The applied friction is then this stopping torque clipped to the available friction budget:
.. math::
\tau_f = \mathrm{clip}(\tau_{f,stop}, -\tau_{fm}, \tau_{fm})
Each model :math:`\mathcal{M}_i` defines :math:`\tau_{fm}` with a different level
of expressiveness.
Servo model + friction model coupling
-------------------------------------
In BAM, simulation combines:
- a servo model :math:`\mathcal{S}` that maps state and target to motor torque
- a friction model :math:`\mathcal{M}` that computes the friction budget
At each step:
1. compute :math:`\tau_m` from control law and motor model
2. compute :math:`\tau_{fm}` from chosen friction model
3. compute :math:`\tau_f` by clipping :math:`\tau_{f,stop}`
4. integrate acceleration from bench dynamics
This makes the framework both identifiable from logs and directly usable in
simulation.
+90
View File
@@ -0,0 +1,90 @@
Identified actuators
====================
Each motor below ships with pre-identified friction parameters. Click a motor
to browse its JSON parameters, or use the **Use in MuJoCo** / **Use in mjlab**
buttons to jump to the integration guide with the code examples pre-filled for
that motor.
.. list-table::
:header-rows: 1
:widths: 14 14 18 28 26
:class: actuators-table
* - Actuator
- Image
- Name
- Description
- Use in simulation
* - **MX-64**
- .. image:: ../_static/actuator_mx64.png
:width: 120px
- ``mx64``
- ROBOTIS Dynamixel MX-64 servo-actuator
(`Parameters <https://github.com/Rhoban/bam/tree/main/bam/params/mx64>`__,
`model <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`__,
`raw data <https://huggingface.co/buckets/Gregwar/bam_data/resolve/mx64_raw.tgz?download=true>`__)
- .. raw:: html
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mujoco_cpu.html?motor=mx64">Use in MuJoCo</a>
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mjlab_gpu.html?motor=mx64">Use in mjlab</a>
* - **MX-106**
- .. image:: ../_static/actuator_mx106.png
:width: 120px
- ``mx106``
- ROBOTIS Dynamixel MX-106 servo-actuator
(`Parameters <https://github.com/Rhoban/bam/tree/main/bam/params/mx106>`__,
`model <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`__,
`raw data <https://huggingface.co/buckets/Gregwar/bam_data/resolve/mx106_raw.tgz?download=true>`__)
- .. raw:: html
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mujoco_cpu.html?motor=mx106">Use in MuJoCo</a>
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mjlab_gpu.html?motor=mx106">Use in mjlab</a>
* - **XL-320**
- .. image:: ../_static/actuator_xl320.png
:width: 120px
- ``xl320``
- ROBOTIS Dynamixel XL-320 servo-actuator
(`Parameters <https://github.com/Rhoban/bam/tree/main/bam/params/xl320>`__,
`model <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`__)
- .. raw:: html
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mujoco_cpu.html?motor=xl320">Use in MuJoCo</a>
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mjlab_gpu.html?motor=xl320">Use in mjlab</a>
* - **XL-330**
- .. image:: ../_static/actuator_xl330.png
:width: 120px
- ``xl330``
- ROBOTIS Dynamixel XL-330 servo-actuator
(`Parameters <https://github.com/Rhoban/bam/tree/main/bam/params/xl330>`__,
`model <https://github.com/Rhoban/bam/blob/main/bam/dynamixel/actuator.py>`__)
- .. raw:: html
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mujoco_cpu.html?motor=xl330">Use in MuJoCo</a>
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mjlab_gpu.html?motor=xl330">Use in mjlab</a>
* - **STS3215 (7.4V)**
- .. image:: ../_static/actuator_feetech_sts3215_7_4V.png
:width: 120px
- ``feetech_sts3215_7_4V``
- Feetech STS3215 servo-actuator (7.4V version)
(`Parameters <https://github.com/Rhoban/bam/tree/main/bam/params/feetech_sts3215_7_4V>`__,
`model <https://github.com/Rhoban/bam/blob/main/bam/feetech/actuator.py>`__,
`raw data <https://huggingface.co/buckets/Gregwar/bam_data/resolve/feetech_sts3215_raw.zip?download=true>`__)
.. raw:: html
<p class="mini-warning">⚠ Only the <code>m1</code> model was identified for this actuator.</p>
- .. raw:: html
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mujoco_cpu.html?motor=feetech_sts3215_7_4V">Use in MuJoCo</a>
<a class="sd-btn sd-btn-outline-primary motor-btn" href="mjlab_gpu.html?motor=feetech_sts3215_7_4V">Use in mjlab</a>
+15
View File
@@ -0,0 +1,15 @@
Examples
========
BAM is used in real open-source robotics projects. If you are looking for
inspiration on how to wire it into a full pipeline, these are good places to
start:
- **mjlab (GPU)** — `MjLab Microban <https://github.com/MarcDcls/mjlab_microban>`_ is an
open-source project, in which the fully open-source 30 cm humanoid Microban
learns to walk. It shows how to plug :class:`bam.mjlab.BamActuatorCfg` into
a vectorized MuJoCo Warp training pipeline.
- **MuJoCo (CPU)** — `Microban <https://github.com/MarcDcls/microban>`_ is
the repository containing the CAD and codebase for the Microban humanoid.
It shows how to use :class:`bam.mujoco.MujocoController` in the MuJoCo
simulation of the robot run by ``make sim``.
+60
View File
@@ -0,0 +1,60 @@
Usage
=====
This section explains how to use identified models in simulation stacks.
.. grid:: 1 2 2 2
:gutter: 3
:class-container: bam-getting-started
.. grid-item-card:: 📇 Identified actuators
:link: actuators
:link-type: doc
:class-card: bam-card
Browse the bundled library of pre-identified servo-actuators and the
friction parameters shipped for each of them.
+++
:doc:`Browse actuators → <actuators>`
.. grid-item-card:: 🖥️ MuJoCo (CPU)
:link: mujoco_cpu
:link-type: doc
:class-card: bam-card
Plug BAM friction models into a standard MuJoCo simulation running on
CPU with :class:`bam.mujoco.MujocoController`.
+++
:doc:`MuJoCo CPU guide → <mujoco_cpu>`
.. grid-item-card:: ⚡ mjlab (MuJoCo GPU)
:link: mjlab_gpu
:link-type: doc
:class-card: bam-card
Integrate BAM into an mjlab pipeline running on GPU with MuJoCo Warp
via :class:`bam.mjlab.BamActuatorCfg`.
+++
:doc:`mjlab GPU guide → <mjlab_gpu>`
.. grid-item-card:: 🤖 Examples
:link: examples
:link-type: doc
:class-card: bam-card
See BAM wired into real open-source robotics projects for end-to-end
inspiration.
+++
:doc:`Browse examples → <examples>`
.. toctree::
:hidden:
actuators
mujoco_cpu
mjlab_gpu
examples
+212
View File
@@ -0,0 +1,212 @@
mjlab (MuJoCo GPU)
==================
This page explains how to integrate BAM friction models into an mjlab
pipeline running on GPU with MuJoCo Warp. The entry point is
:class:`bam.mjlab.BamActuatorCfg`.
Installation
------------
BAM is available on PyPI. Install it with the ``mjlab`` extra, which pulls in
mjlab together with mujoco, mujoco-warp and torch:
.. code-block:: text
pip install better-actuator-models[mjlab]
Or, with `uv <https://docs.astral.sh/uv/>`_:
.. code-block:: text
uv add "better-actuator-models[mjlab]"
BAM is compatible with mjlab 1.3.
Registering the BAM init event
------------------------------
First, be sure to register the ``bam_init`` function as a startup event in your environment:
.. code-block:: python
from bam.mjlab import bam_init
# cfg is your mjlab task configuration
cfg.events["bam_init"] = EventTermCfg(func=bam_init, mode="startup")
Instantiating the config
------------------------
:class:`~bam.mjlab.BamActuatorCfg` is a dataclass that plugs into mjlab's
actuator system. When an ``Entity`` is built, it instantiates a
:class:`~bam.mjlab.BamActuator` that runs the full BAM pipeline — voltage
control law, DC motor torque, and BAM friction budget — fully vectorized
over all parallel environments via PyTorch tensors.
Two approaches are available, mutually exclusive:
**Bundled motor:**
.. code-block:: python
from bam.mjlab import BamActuatorCfg
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
)
**Custom JSON** (parameters produced by ``bam.fit``):
.. code-block:: python
actuator_cfg = BamActuatorCfg(
json_path="params/my_motor/m6.json",
target_names_expr=(r".*",),
)
The ``target_names_expr`` field is a tuple of regex patterns that select
which actuated joints this config controls.
- Supported bundled motors: see the :doc:`list of identified actuators <actuators>`.
- Supported model variants: ``"m1"`` through ``"m6"`` (see :doc:`../theory/models`).
Voltage and P-gain overrides
-----------------------------
By default, the supply voltage and firmware P-gain are read from the
parameter JSON. They can be overridden at config level:
.. code-block:: python
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
vin=7.5, # supply voltage [V]
kp_fw=125.0, # firmware P-gain
)
Domain randomization
--------------------
:class:`~bam.mjlab.BamActuatorCfg` supports per-environment randomization
of two physical quantities that are naturally variable across hardware units
or charge states.
**Battery voltage** — sample a different supply voltage for each environment
at startup:
.. code-block:: python
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
vin_range=(7.0, 8.0), # sampled uniformly at startup [V]
)
``vin_range`` takes precedence over ``vin`` when both are set.
**Voltage drop resistance** — model battery + cable resistance with a per-env
equivalent resistor between the battery and the motors:
.. math::
V_\text{eff} = V_\text{in} - R_\text{drop} \, I,
\qquad
I = \frac{1}{K_t} \sum_i |\tau_i|
where :math:`R_\text{drop}` (``vin_drop_resistance_range``) is the combined
battery + wire resistance in ohms, and the current :math:`I` is estimated from
the actuator torques using the torque constant :math:`K_t`. Randomizing this
resistance captures variability in cable length or connector quality across
units:
.. code-block:: python
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
vin_range=(7.0, 8.0),
vin_drop_resistance_range=(0.05, 0.15), # [Ohm] ~100 mOhms of wire & battery resistance
vin_min=6.0, # hard lower bound [V]
)
Both ranges are sampled once at initialization and held constant across
episode resets.
.. warning::
The voltage drop is computed independently by each
:class:`~bam.mjlab.BamActuator` from its own joints' current draw. If several
actuator configs share the same physical battery, their currents are **not**
summed together, so the modeled drop underestimates the real one. Group all
joints powered by the same battery under a single :class:`~bam.mjlab.BamActuatorCfg`
if you need the shared-supply behavior. In the current implementation, only one model
can be used per actuator config, voltage drop will not function properly if different motors
are mixed in the same config.
Command delay
-------------
BAM inherits mjlab's command delay system, which models the latency between
policy output and motor response (e.g. communication bus latency, firmware
scheduling). The lag is expressed in simulation steps and can be randomized
per environment:
.. code-block:: python
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
delay_min_lag=1, # always at least 1 step of delay
# = 5ms with mjlab's default 200Hz control loop
delay_max_lag=3, # up to 3 steps, randomized per env
# = 15ms with mjlab's default 200Hz control loop
)
Setting ``delay_min_lag == delay_max_lag`` gives a fixed, deterministic delay.
Leave both at ``0`` (default) to disable delay entirely.
Passing the config to an Entity
--------------------------------
Pass the config to the ``actuator_cfgs`` argument of an mjlab ``Entity``:
.. code-block:: python
import mjlab
actuator_cfg = BamActuatorCfg(
motor_name="{actuator}",
model="m6",
target_names_expr=(r".*",),
kp_fw=125,
vin_range=(7.0, 8.0),
vin_drop_resistance_range=(0.05, 0.15), # [Ohm]
vin_min=6.0,
delay_min_lag=1,
delay_max_lag=3,
)
entity = mjlab.Entity(
xml_path="robot.xml",
actuator_cfgs=(actuator_cfg,),
)
mjlab calls :meth:`~bam.mjlab.BamActuatorCfg.build` internally to create the
:class:`~bam.mjlab.BamActuator` and wire it into the simulation graph.
API reference
-------------
- :class:`bam.mjlab.BamActuatorCfg`
- :class:`bam.mjlab.BamActuator`
- :func:`bam.model.load_model`
+218
View File
@@ -0,0 +1,218 @@
MuJoCo (CPU)
============
This page explains how to plug BAM friction models into a standard MuJoCo
simulation running on CPU. The entry point is :class:`bam.mujoco.MujocoController`.
Installation
------------
BAM is available on PyPI. Install it with the ``mujoco`` extra to pull in the
MuJoCo dependency:
.. code-block:: text
pip install better-actuator-models[mujoco]
Or, with `uv <https://docs.astral.sh/uv/>`_:
.. code-block:: text
uv add "better-actuator-models[mujoco]"
Overview
--------
At each simulation step, :class:`~bam.mujoco.MujocoController` does three things:
1. Optionally lowers the supply voltage by a drop proportional to the previous
step's load, to model battery + cable resistance.
2. Computes the motor torque from a firmware-like P-controller — optionally
clipping it to the firmware current limit — and applies it via
``mj_data.ctrl``.
3. Evaluates the BAM friction model and writes the result into
``mj_model.dof_frictionloss`` and ``mj_model.dof_damping``.
Loading a model
---------------
Use :func:`bam.model.load_model` to obtain a :class:`~bam.model.Model` object.
Two approaches are available.
**Bundled motor** — the library ships identified parameters for a set of common
servos:
.. code-block:: python
from bam.model import load_model
model = load_model(motor_name="{actuator}", model="m6")
- Supported motor names: see the :doc:`list of identified actuators <actuators>`.
- Supported model variants: ``"m1"`` through ``"m6"`` (see :doc:`../theory/models`).
**Custom JSON** — parameters produced by your own identification run:
.. code-block:: python
model = load_model("path/to/params.json")
XML setup
---------
Each actuator must be declared as a ``motor`` in the MJCF file (not
``position`` or ``velocity``). BAM overwrites ``frictionloss``, ``damping``,
and ``armature`` at runtime, so any value set in the XML will be ignored.
.. code-block:: xml
<actuator>
<motor name="joint_1" joint="joint_1" gear="1"/>
...
<motor name="joint_n" joint="joint_n" gear="1"/>
</actuator>
Instantiating the controller
-----------------------------
.. code-block:: python
import mujoco
from bam.mujoco import MujocoController
mj_model = mujoco.MjModel.from_xml_file("robot.xml")
mj_data = mujoco.MjData(mj_model)
controller = MujocoController(
model=model,
actuator=["joint_1", ..., "joint_n"], # must match the motor name in the XML
mujoco_model=mj_model,
mujoco_data=mj_data,
)
The ``actuator`` argument can take a single string or a list of strings, which
allows the same motor model to drive multiple joints. Each string must
match the ``name`` attribute of the ``<motor>``.
Simulation loop
---------------
Inside the loop, call :meth:`~bam.mujoco.MujocoController.set_q_target` to
provide the desired joint angle, then :meth:`~bam.mujoco.MujocoController.update`
before every ``mj_step``:
.. code-block:: python
mujoco.mj_resetData(mj_model, mj_data)
joint_names = ["joint_1", ..., "joint_n"]
target_angles = [...]
while True:
for joint_name, target_angle in zip(joint_names, target_angles):
controller.set_q_target(joint_name, target_angle)
controller.update()
mujoco.mj_step(mj_model, mj_data)
Voltage drop (optional)
-----------------------
Real batteries and cables have an internal resistance that causes a voltage
drop under load. BAM models this as an equivalent resistor placed between the
battery and the motors:
.. math::
V_\text{eff} = V_\text{in} - R_\text{drop} \, I,
\qquad
I = \frac{1}{K_t} \sum_i |\tau_i|
where ``vin_drop_resistance`` is :math:`R_\text{drop}` (the combined battery +
wire resistance, in ohms), and the current :math:`I` is estimated from the
actuator torques using the torque constant :math:`K_t`, summed over all
controlled joints. A hard lower bound ``vin_min`` can be set to prevent the
effective voltage from collapsing under heavy load:
.. code-block:: python
controller = MujocoController(
model=model,
actuator=["joint_1", ..., "joint_n"],
mujoco_model=mj_model,
mujoco_data=mj_data,
vin_drop_resistance=0.1, # 100 mOhms of wire & battery resistance
vin_min=6.0, # [V]
)
.. warning::
The voltage drop is computed independently by each
:class:`~bam.mujoco.MujocoController` from its own joints' current draw.
If several controllers share the same physical battery, their currents are
**not** summed together, so the modeled drop underestimates the real one.
Group all joints powered by the same battery under a single controller if you
need the shared-supply behavior.
Multi-actuator config file
--------------------------
For robots with many joints, :func:`bam.mujoco.load_config` loads a JSON
configuration file that maps each group of joints to a model:
.. code-block:: python
from bam.mujoco import load_config
controllers, dof_to_controller = load_config(
path="config.json",
mujoco_model=mj_model,
mujoco_data=mj_data,
kp=125.0,
vin=7.5,
)
The config file has the following structure:
.. code-block:: json
{
"arm": {
"dofs": ["shoulder", "elbow"],
"model": {
"kt": 1.6224667906987444,
"R": 3.949433673232461,
"armature": 0.011951238325312509,
"friction_base": 0.09038677246291783,
"friction_viscous": 0.011691602145974832,
"model": "m1",
"actuator": "mx64"
},
"error_gain": 1.0,
"max_pwm": 885
},
"leg": {
"dofs": ["hip", "knee", "ankle"],
"model": {
"kt": 2.1913757006745245,
"R": 2.9649903987776804,
"armature": 0.026609234235148084,
"friction_base": 0.10352026623606064,
"friction_viscous": 0.03520238029013507,
"model": "m1",
"actuator": "mx106"
},
"error_gain": 1.0,
"max_pwm": 885
}
}
``controllers`` is a dict keyed by group name; ``dof_to_controller`` maps each
DOF name back to its group.
API reference
-------------
- :class:`bam.mujoco.MujocoController`
- :func:`bam.mujoco.load_config`
- :func:`bam.model.load_model`
+28
View File
@@ -0,0 +1,28 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"
if ! command -v inotifywait >/dev/null 2>&1; then
echo "inotifywait not found. Install inotify-tools (e.g. sudo apt install inotify-tools)."
exit 1
fi
if [[ ! -x ../.venv/bin/sphinx-build ]] && ! command -v sphinx-build >/dev/null 2>&1; then
echo "sphinx-build not found. Run: uv venv ../.venv && uv pip install --python ../.venv/bin/python -r requirements.txt"
exit 1
fi
make html
echo "Starting watching..."
pkill -f "python3 -m http.server 8080" >/dev/null 2>&1 || true
cd _build/html
python3 -m http.server 8080 --bind 127.0.0.1 &
cd ../..
while true
do
inotifywait *.rst */*.rst --recursive
make html
sleep 0.5
done