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
+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