Import upstream snapshot 57d13ead53206a6bf0db3d66f86506ae8c2ce01a
Upstream: https://github.com/pollen-robotics/bam Upstream-Commit: 57d13ead53206a6bf0db3d66f86506ae8c2ce01a Upstream-Branch: mjlab_frictionloss
@@ -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)
|
||||
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 27 KiB |
@@ -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;
|
||||
}
|
||||
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 104 KiB |
|
After Width: | Height: | Size: 9.0 KiB |
|
After Width: | Height: | Size: 7.9 MiB |
|
After Width: | Height: | Size: 6.7 MiB |
|
After Width: | Height: | Size: 5.3 MiB |
@@ -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();
|
||||
}
|
||||
})();
|
||||
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 175 KiB |
@@ -0,0 +1,7 @@
|
||||
API Reference
|
||||
=============
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
../autoapi/bam/index
|
||||
@@ -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",
|
||||
}
|
||||
],
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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.
|
||||
@@ -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>
|
||||
@@ -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``.
|
||||
@@ -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
|
||||
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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
|
||||