Files
Mujoco_WASM/doc/dcmotor/dcmotor.tex
T
Yuval Tassa 2f1843f4a7 Redesign the dcmotor controller: setpoint inputs, torque-space gains.
The dcmotor input block is any subset of the canonical list [pos, vel,
ff, voltage], selected with input="pos vel ff voltage" and recorded as
mjtCtrlInput bits in actuator_ctrlspec like pid. Tokens are required in
canonical order: the attribute denotes a set, the block always packs
canonically, and accepting permutations invites reading the string as a
layout choice. The mode flag in gainprm[8] is retired (reserved,
written 0).

Controller gains are now in torque space, as for pid: the controller
commands tau = kp*(q*-l) + kd*(v*-ldot) + ki*x_I + tau_ff over the
present inputs (absent setpoints frozen at zero) and converts to drive
voltage V = R/K * tau + K*ldot. The second term compensates back-EMF,
as the current loop of a real torque-mode driver does (torque commands
are current commands): commanded torque is delivered exactly until a
limit binds, and the torque-speed envelope emerges from the Vmax clamp.
The map uses the nameplate R: thermal resistance growth is not
compensated, so a hot motor under-delivers by R/R(T). A stateless
setpoint dcmotor now matches <pid> exactly, for any K and R; the old
back-EMF droop remains available as the physical behavior of the raw
voltage path. Voltage-space datasheet gains convert by K/R. Controller
inputs require a positive motor constant (the map divides by K), and
controller gains require a controller input.

ff and voltage are distinct inputs, different in kind: ff is a torque
feedforward added to the controller output, uniform with pid's ff
(feedforward in the actuator's output space), while voltage is the raw
terminal voltage of the physical device, injected downstream of the
controller and its Vmax clamp, unclamped (ctrlrange bounds it if
desired). input="voltage" is the default: the plain voltage-commanded
motor, whose behavior is unchanged by this commit. The integrator
always accumulates position error; the old velocity mode's integral
term, ki*(int(u)dt - theta), which tracked the integral of the velocity
command, is retired without replacement, keeping ki mode-independent --
commanded integrated velocity belongs to an integrator activation
state, not to controller gains. slewmax rate-limits the first controller
input -- position setpoint (rad/s), velocity setpoint (rad/s^2) or torque
feedforward (N*m/s), each a real driver feature (reference ramping,
ramped-velocity and ramped-torque input modes); the raw voltage input
is never rate-limited and slewmax requires a controller input.

input="none" selects the empty signature: the actuator owns no controls
at all (nu = 0 is now legal with actuators present) and is purely
passive -- LuGre friction, cogging and back-EMF braking as passive
joint forces. This exists because auxiliary dynamic states (the LuGre
bristle) attach to actuators, not joints. The terminal voltage is
identically zero, i.e. a shorted motor (dynamic braking); motorconst=0
decouples the electrical branch. mjINPUT_NONE is a distinct enum value
because ctrlspec = 0 means "unset, use the type default". History and
delay require an input; the controller voltage override and input read
in mj_fwdActuation are gated on a nonempty block.

The analytic velocity derivative of the controller becomes
dV/dw = -kd*R/K + K, whose second term cancels the back-EMF bias
exactly: the net damping of an unclipped torque-mode motor is -kd, and
of a voltage-mode or passive motor -K^2/R. Viewers label inputs via
mj_actuatorInputName: pos, vel, ff, voltage.

The dcmotor LaTeX design doc is updated accordingly: torque-space
units, the tau->V map and its saturation-generated envelope, the
input-block pipeline figure, and a Passive Operation section.

PiperOrigin-RevId: 965795351
Change-Id: Ibc308ca21bd6bad014e77f950ee08feaad449b73
2026-08-17 00:43:48 -07:00

1370 lines
63 KiB
TeX

% Copyright 2026 DeepMind Technologies Limited
%
% Licensed under the Apache License, Version 2.0 (the "License");
% you may not use this file except in compliance with the License.
% You may obtain a copy of the License at
%
% http://www.apache.org/licenses/LICENSE-2.0
%
% Unless required by applicable law or agreed to in writing, software
% distributed under the License is distributed on an "AS IS" BASIS,
% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
% See the License for the specific language governing permissions and
% limitations under the License.
\documentclass[10pt, a4paper, twocolumn]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{roboto-mono}
\usepackage{relsize}
\let\oldtexttt\texttt
\renewcommand{\texttt}[1]{{\smaller\oldtexttt{#1}}}
\usepackage{amsmath, amssymb}
\usepackage{multicol}
\usepackage{geometry}
\geometry{margin=0.75in}
\usepackage{titlesec}
\titlespacing*{\section}{0pt}{1.5ex plus 0.5ex minus 0.2ex}{1ex plus 0.2ex}
\titlespacing*{\subsection}{0pt}{1.2ex plus 0.4ex minus 0.2ex}{0.8ex plus 0.2ex}
\setlength{\parskip}{0.4ex plus 0.1ex minus 0.1ex}
\usepackage{booktabs}
\usepackage{enumitem}
\setlist[itemize]{label=\scalebox{0.8}{$\bullet$}}
\usepackage{float}
\usepackage{titling}
\setlength{\droptitle}{-4em}
\usepackage{stfloats}
\usepackage{url}
\renewcommand{\UrlFont}{\small\ttfamily}
\usepackage{tikz}
\usepackage{pgfplots}
\pgfplotsset{compat=1.18}
\usepgfplotslibrary{fillbetween}
\usepackage{hyperref}
\hypersetup{colorlinks=true, linkcolor=blue, urlcolor=blue, citecolor=blue}
\usepackage{caption}
\usepackage{subcaption}
\captionsetup{font=footnotesize, labelfont=footnotesize}
\usepackage{xcolor}
\usepackage{listings}
\lstset{
language=C,
basicstyle=\footnotesize\ttfamily,
keywordstyle=\bfseries\color{blue!70!black},
commentstyle=\itshape\color{gray},
stringstyle=\color{red!60!black},
numbers=left,
numberstyle=\tiny\color{gray},
numbersep=5pt,
frame=single,
framerule=0.4pt,
rulecolor=\color{gray!40},
backgroundcolor=\color{gray!5},
breaklines=true,
columns=fullflexible,
keepspaces=true,
showstringspaces=false,
tabsize=2,
xleftmargin=1.5em,
framexleftmargin=1.5em,
aboveskip=0.8em,
belowskip=0.5em,
morekeywords={mjtNum, mjModel, mjData, mjtByte},
}
\newcommand{\atR}{\texttt{resistance}}
\newcommand{\atK}{\texttt{motorconst}}
\newcommand{\atKt}{\texttt{motorconst:Kt}}
\newcommand{\atKe}{\texttt{motorconst:Ke}}
\newcommand{\atVM}{\texttt{nominal:voltage}}
\newcommand{\atSTALL}{\texttt{nominal:stall\_torque}}
\newcommand{\atNLS}{\texttt{nominal:no\_load\_speed}}
\newcommand{\atTMAX}{\texttt{saturation:torque}}
\newcommand{\atIMAX}{\texttt{saturation:current}}
\newcommand{\atVMAX}{\texttt{controller:Vmax}}
\newcommand{\atCRATE}{\texttt{saturation:current\_rate}}
\newcommand{\atKP}{\texttt{controller:kp}}
\newcommand{\atKI}{\texttt{controller:ki}}
\newcommand{\atKD}{\texttt{controller:kd}}
\newcommand{\atSLEW}{\texttt{controller:slewmax}}
\newcommand{\atIMAXINT}{\texttt{controller:Imax}}
\newcommand{\atL}{\texttt{inductance:L}}
\newcommand{\atTE}{\texttt{inductance:timeconst}}
\newcommand{\atCOGA}{\texttt{cogging:amplitude}}
\newcommand{\atCOGP}{\texttt{cogging:poles}}
\newcommand{\atCOGPH}{\texttt{cogging:phase}}
\newcommand{\atRT}{\texttt{thermal:resistance}}
\newcommand{\atTC}{\texttt{thermal:capacitance}}
\newcommand{\atTT}{\texttt{thermal:timeconst}}
\newcommand{\atALPHA}{\texttt{thermal:tempcoef}}
\newcommand{\atTREF}{\texttt{thermal:reftemp}}
\newcommand{\atTAMB}{\texttt{thermal:ambient}}
\newcommand{\atSIG}{\texttt{lugre:stiffness}}
\newcommand{\atSIGD}{\texttt{lugre:damping}}
\newcommand{\atTAUC}{\texttt{lugre:coulomb}}
\newcommand{\atTAUS}{\texttt{lugre:static}}
\newcommand{\atWS}{\texttt{lugre:stribeck}}
\title{MuJoCo DC Motor Model}
\author{Google DeepMind}
\date{}
\begin{document}
\maketitle
\noindent We review DC motors and describe MuJoCo's \href{https://mujoco.readthedocs.io/en/stable/XMLreference.html#actuator-dcmotor}{\texttt{dcmotor} actuator}. The equations are derived for brushed motors but apply equally to brushless ones, where electronic commutation reduces to an equivalent circuit.
%=============================================================================
% BACKGROUND
%=============================================================================
\section{Background}
\label{sec:background}
We use SI units throughout, but any coherent system of units applies. We assume motion is rotational; for linear motion replace radians with meters as required.
% ---------------------------------------------------------------------------
% Electromagnetic Model
% ---------------------------------------------------------------------------
\subsection{Electromagnetic Model}
\label{sec:electromagnetics}
The key electro-mechanical variables are
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & Units \\
\midrule
$v$ & Applied voltage & Volt \\
$i$ & Current & Ampere \\
$\omega$ & Angular velocity & radian/second \\
$\tau$ & Output torque & Newton $\cdot$ meter \\
\bottomrule
\end{tabular}
\end{table}
\noindent and the key constants are
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & Units \\
\midrule
$R$ & Resistance & Ohm \\
$K_t$ & Torque constant & Newton $\cdot$ meter/Ampere \\
$K_e$ & Back-EMF constant & Volt $\cdot$ second/radian \\
\bottomrule
\end{tabular}
\end{table}
\noindent The quasi-static model~\cite{hughes2019, maxon_formulas, simscape_dcmotor} assumes instantaneous electrical dynamics: current and torque are direct functions of voltage and velocity. The constitutive equations are the voltage balance \eqref{eq:voltage} and the torque law \eqref{eq:torque_law}:
\begin{subequations}
\label{eq:motor_laws}
\begin{align}
v &= i \, R + K_e \, \omega \label{eq:voltage} \\
\tau &= K_t i \label{eq:torque_law}
\end{align}
\end{subequations}
\noindent Solving for current and substituting, we have
\begin{equation}
\tau = \frac{K_t}{R} (v - K_e \, \omega)
\label{eq:torque_speed}
\end{equation}
\noindent Output torque is proportional to the difference between applied and back-EMF voltage $v_{\text{back}} = K_e \, \omega$ (Figure~\ref{fig:torque_speed}).
\paragraph{Electrical constants.}
Fundamentally, both $K_t$ and $K_e$ arise from the same physical quantity: the magnetic flux $\Phi$ of the coil. Faraday's law gives $v_{\text{back}} = \Phi \, \omega$ and the Lorentz force gives $\tau = \Phi \, i$, so in SI units:
\begin{equation*}
K_e = K_t
\label{eq:ke_eq_kt}
\end{equation*}
\noindent This can also be seen from energy conservation: $P_e = i \, (K_e \, \omega) = (K_t \, i) \, \omega = P_m$.
\pagebreak
\noindent Note the dimensions match:
\vspace*{-\abovedisplayskip}
\begin{equation*}
\frac{\text{Volt}}{\text{radian}/\text{second}}
= \frac{\text{Joule}}{\text{Coulomb}/\text{second}}
= \frac{\text{Newton} \cdot \text{meter}}{\text{Ampere}}
\end{equation*}
\noindent If these constants are the same, why have both? Two reasons. First, datasheets typically use mixed units ($K_e$ in RPM/V, $K_t$ in mN$\cdot$m/A), giving different values for the same physical quantity. Second, $K_t$ and $K_e$ are measured differently: $K_t$ by locking the rotor and measuring torque per Ampere; $K_e$ by spinning the rotor and measuring open-circuit Volts per radian/second. In the first case, high currents can lead to magnetic field saturation in the core, causing the effective $K_t$ to drop below $K_e$. The equality $K_e = K_t$ thus assumes $\Phi$ independent of $i$. We make this assumption for now and use a single motor constant $K \equiv K_t = K_e$ throughout the remainder of this document and internally in MuJoCo, but see note at end of \S\ref{sec:dcmotor}.
\begin{figure}[H]
\centering
\begin{tikzpicture}
\pgfmathsetmacro{\taus}{1.0}
\pgfmathsetmacro{\wz}{1.0}
\begin{axis}[
width=0.9\columnwidth, height=0.55\columnwidth,
axis lines=left,
clip=false,
xlabel={$\omega$}, ylabel={$\tau$},
xmin=0, xmax={\wz*1.15}, ymin=0, ymax={\taus*1.15},
xtick={\wz}, xticklabels={$\omega_0$},
ytick={\taus}, yticklabels={$\tau_0$},
tick style={thick},
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
]
\addplot[thick, blue!15] coordinates {(0,\taus*0.7) (\wz*0.7,0)};
\addplot[thick, blue!25] coordinates {(0,\taus*0.8) (\wz*0.8,0)};
\addplot[thick, blue!50] coordinates {(0,\taus*0.9) (\wz*0.9,0)};
\addplot[thick, blue] coordinates {(0,\taus) (\wz,0)};
\node[font=\scriptsize, text=gray, align=center]
at (axis cs: \wz*0.25, \taus*0.2)
{decreasing\\ voltage};
\draw[->, thick, gray] (axis cs: \wz*0.4, \taus*0.55)
-- (axis cs: \wz*0.4, \taus*0.15);
\node[font=\scriptsize] at (axis cs: \wz*0.4, -0.08)
{Speed};
\node[font=\scriptsize, rotate=90] at (axis cs: -0.04, \taus*0.4)
{Torque};
\end{axis}
\end{tikzpicture}
\caption{Torque-speed relationship \eqref{eq:torque_speed} at fixed voltage. As voltage decreases, the maximum torque and speed decrease linearly.}
\label{fig:torque_speed}
\end{figure}
\paragraph{Current Saturation.}
A maximum current rating $i_{\max}$ limits the output torque:
\begin{equation}
\tau = \text{clip}\!\left(\frac{K}{R}(v - K \, \omega),\;
\pm K \, i_{\max} \right)
\label{eq:saturation}
\end{equation}
where the maximum torque $\tau_{\max} = K \, i_{\max}$. The feasible torque-speed envelope forms a parallelogram:
\begin{figure}[H]
\centering
\begin{tikzpicture}
\pgfmathsetmacro{\taus}{1.3}
\pgfmathsetmacro{\wz}{1.0}
\pgfmathsetmacro{\taumax}{0.7}
\pgfmathsetmacro{\slope}{\taus/\wz}
\pgfmathsetmacro{\wcu}{(\taus-\taumax)/\slope}
\pgfmathsetmacro{\wcl}{(\taus+\taumax)/\slope}
\pgfmathsetmacro{\wext}{1.5}
\pgfmathsetmacro{\dexthi}{\taus+\slope*\wext}
\pgfmathsetmacro{\dextlo}{\taus-\slope*\wext}
\begin{axis}[
width=0.9\columnwidth, height=0.6\columnwidth,
axis lines=middle,
xlabel={$\omega$}, ylabel={$\tau$},
xmin=-1.6, xmax=1.6, ymin=-1.6, ymax=1.6,
xtick={-\wz, \wz}, xticklabels={$-\omega_0$, {}},
ytick={-\taus, \taus},
yticklabels={$-\tau_0$, $\tau_0$},
tick style={thick},
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
]
\fill[blue, opacity=0.08]
(-\wcl, \taumax) -- (\wcu, \taumax) -- (\wcl, -\taumax)
-- (-\wcu, -\taumax) -- cycle;
\addplot[thick, dashed, gray] coordinates {(-\wext, \dexthi) (\wext, \dextlo)};
\addplot[thick, dashed, gray] coordinates {(-\wext, -\dextlo) (\wext, -\dexthi)};
\addplot[thick, dashed, gray] coordinates {(-1.55, \taumax) (1.55, \taumax)};
\addplot[thick, dashed, gray] coordinates {(-1.55, -\taumax) (1.55, -\taumax)};
\addplot[thick, blue] coordinates
{(-\wcl, \taumax) (\wcu, \taumax) (\wcl, -\taumax) (-\wcu, -\taumax)
(-\wcl, \taumax)};
\node[font=\scriptsize, anchor=south] at (axis cs: \wz, \taumax)
{$\tau_{\max}$};
\node[font=\scriptsize, anchor=north] at (axis cs: -\wz, -\taumax)
{$-\tau_{\max}$};
\draw[thick, dashed, gray] (axis cs: \wz, 0) -- (axis cs: \wz, -\taumax);
\draw[thick, dashed, gray] (axis cs: -\wz, 0) -- (axis cs: -\wz, \taumax);
\node[font=\normalsize, anchor=south] at (axis cs: \wz, 0.05) {$\omega_0$};
\end{axis}
\end{tikzpicture}
\caption{Torque-speed envelope with current saturation~\eqref{eq:saturation}.}
\label{fig:saturation}
\end{figure}
\noindent Note that datasheets typically distinguish two current limits. The \emph{continuous} (or \emph{nominal}) current $i_{\max}$ is the thermal limit: the maximum current the motor can sustain indefinitely without exceeding its maximum winding temperature. The \emph{peak} current $i_{\text{peak}}$ is a higher short-term limit, typically 5--10$\times$ the continuous value, constrained by demagnetization or commutation limits.
\begin{table}[H]
\centering
\footnotesize
\setlength{\tabcolsep}{3pt}
\renewcommand{\arraystretch}{1.2}
\begin{tabular}{@{}llll@{}}
\toprule
Symbol & Description & Condition & Formula/note\\
\midrule
$\tau_0$ & Stall Torque & $\omega=0$ & $\tau_0 = Kv / R$ \\
$\omega_0$ & No-Load Speed & $\tau_{\text{load}}=0$ &
$\omega_0 \approx v / K$ \\
$\partial\omega / \partial\tau$ & Gradient & Slope &
$-R/K^2$ \\
$i_{\max}$ & Maximum Current & Limit & Thermal limit \\
$\tau_{\max}$ & Maximum Torque & Limit & $\tau_{\max} = K \, i_{\max}$ \\
\bottomrule
\end{tabular}
\caption{Named constants derived from the motor equations.}
\label{tab:electromech_constants}
\end{table}
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Inductance}
\label{sec:inductance}
Including the effects of winding inductance $L$ (Henry) means treating the current $i$ as a state variable:
\begin{equation}
v = L \, \frac{di}{dt} + i \, R + K \, \omega
\label{eq:inductance}
\end{equation}
The change in current is proportional to the voltage and negatively proportional to both the instantaneous current and the rotation velocity. The time constant of this ODE is $t_e = L/R$. If $t_e \ll \Delta t$ (the simulation timestep), the current equilibrates within a single step and the quasi-static approximation~\eqref{eq:torque_speed} is adequate.
Motor drivers often impose a hard limit on $di/dt$ to protect windings and electronics, bounding the torque ramp rate to $K \cdot (di/dt)_{\max}$.
% ---------------------------------------------------------------------------
% Mechanical Model
% ---------------------------------------------------------------------------
\subsection{Mechanical Model}
\label{sec:mechanical}
Several purely mechanical phenomena affect the motor's behavior and delivered torque, warping the electromagnetic performance envelope.
\paragraph{Mechanical losses.}
These reduce the net torque available at the shaft: $\tau_{\text{net}} = \tau_{\text{elec}} - \tau_{\text{loss}}$.
\begin{itemize}
\item \textbf{Coulomb:} Constant torque opposing rotation (dry friction).
$\tau_{\text{loss}} = \tau_c \, \text{sgn}(\omega)$. Discontinuous; already available in MuJoCo as \texttt{frictionloss}.
\item \textbf{Viscous:} Drag is a smooth function of speed $\tau_{\text{loss}} = b(\omega)$. The simplest model is linear, with drag proportional to speed $\tau_{\text{loss}} = B \, \omega$, but higher order terms may be needed for higher-fidelity models e.g., $\tau_{\text{loss}} = B_1 \, \omega + B_2 \, \omega |\omega| + B_3 \, \omega^3 + \dots$.
\end{itemize}
\noindent Datasheets report the \emph{no-load current} $i_0$: the current drawn when spinning freely at no-load speed $\omega_0$. At steady state, the electromagnetic torque balances all mechanical losses:
\begin{equation}
K \, i_0 = \tau_c + B \, \omega_0
\label{eq:noload}
\end{equation}
This provides one constraint on two unknowns ($\tau_c$ and $B$). Without additional data, the user must either assume one dominates or obtain friction measurements at multiple speeds. In MuJoCo terms, $\tau_c$ maps to \texttt{frictionloss} and $B$ to \texttt{damping}.
Combining current saturation with both mechanical losses, the net torque is:
\begin{equation*}
\tau_{\text{net}} = \text{clip}\!\left( \frac{K}{R}(v - K \, \omega),\;
\pm K\, i_{\max} \right) - B \, \omega - \tau_c \, \text{sgn}(\omega)
\label{eq:net_torque}
\end{equation*}
The clipping applies to the electrical torque (current limit), while both friction terms are mechanical losses applied to the output post-clipping. Viscous drag $-B\omega$ tilts the envelope; Coulomb friction $-\tau_c\,\text{sgn}(\omega)$ shifts the right half ($\omega > 0$) down and the left half ($\omega < 0$) up, creating a $2\tau_c$ discontinuity at $\omega = 0$ (Figure~\ref{fig:drag_linear}).
\begin{figure}[H]
\centering
\begin{subfigure}[t]{\columnwidth}
\centering
\begin{tikzpicture}
\pgfmathsetmacro{\taus}{1.3}
\pgfmathsetmacro{\wz}{1.0}
\pgfmathsetmacro{\taumax}{0.7}
\pgfmathsetmacro{\bvis}{0.3}
\pgfmathsetmacro{\tauf}{0.2}
\pgfmathsetmacro{\slope}{\taus/\wz}
\pgfmathsetmacro{\wcu}{(\taus-\taumax)/\slope}
\pgfmathsetmacro{\wcl}{(\taus+\taumax)/\slope}
\pgfmathsetmacro{\wext}{1.5}
\pgfmathsetmacro{\dragL}{\bvis*\wext}
\pgfmathsetmacro{\dragR}{-\bvis*\wext}
%% Right half vertices (ω ≥ 0): shifted down by τ_f
\pgfmathsetmacro{\Ra}{\taumax-\tauf}
\pgfmathsetmacro{\Rb}{\taumax-\bvis*\wcu-\tauf}
\pgfmathsetmacro{\Rc}{-\taumax-\bvis*\wcl-\tauf}
\pgfmathsetmacro{\Rd}{-\taumax-\tauf}
%% Left half vertices (ω ≤ 0): shifted up by τ_f
\pgfmathsetmacro{\La}{\taumax+\bvis*\wcl+\tauf}
\pgfmathsetmacro{\Lb}{\taumax+\tauf}
\pgfmathsetmacro{\Lc}{-\taumax+\tauf}
\pgfmathsetmacro{\Ld}{-\taumax+\bvis*\wcu+\tauf}
\begin{axis}[
width=0.9\columnwidth, height=0.6\columnwidth,
axis lines=middle,
xlabel={$\omega$}, ylabel={$\tau$},
xmin=-1.8, xmax=1.8, ymin=-1.8, ymax=1.8,
xtick=\empty, ytick=\empty,
tick style={thick},
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
]
\fill[blue, opacity=0.08]
(0, \Lb) -- (-\wcl, \La) -- (-\wcu, \Ld) -- (0, \Lc)
-- (0, \Rd) -- (\wcl, \Rc) -- (\wcu, \Rb) -- (0, \Ra) -- cycle;
\addplot[thick, blue] coordinates
{(0, \Lb) (-\wcl, \La) (-\wcu, \Ld) (0, \Lc)
(0, \Rd) (\wcl, \Rc) (\wcu, \Rb) (0, \Ra) (0, \Lb)};
\addplot[thick, dashed, gray] coordinates {(0.01, {-\tauf-\bvis*0.01}) (\wext, {-\tauf+\dragR})};
\addplot[thick, dashed, gray] coordinates {(-\wext, {\tauf+\dragL}) (-0.01, {\tauf+\bvis*0.01})};
\node[font=\footnotesize, anchor=north west] at (axis cs: -1.75, -0.45)
{$-B\omega - \tau_c\,\text{sgn}(\omega)$};
\draw[->, gray, thick] (axis cs: -1.4, -0.45) -- (axis cs: -1.3, {\tauf+\bvis});
\pgfmathsetmacro{\gapmid}{(\Ra+\Lb)/2}
\draw[thick, <->, gray] (axis cs: 0.12, \Ra) -- (axis cs: 0.12, \Lb);
\node[font=\scriptsize, anchor=west] at (axis cs: 0.18, \gapmid)
{$2\tau_c$};
\end{axis}
\end{tikzpicture}
\caption{Linear viscous drag and Coulomb friction.}
\label{fig:drag_linear}
\end{subfigure}
\vspace{0.5em}
\begin{subfigure}[t]{\columnwidth}
\centering
\begin{tikzpicture}
\pgfmathsetmacro{\taus}{1.3}
\pgfmathsetmacro{\wz}{1.0}
\pgfmathsetmacro{\taumax}{0.7}
\pgfmathsetmacro{\Bone}{0.15}
\pgfmathsetmacro{\Btwo}{0.35}
\pgfmathsetmacro{\slope}{\taus/\wz}
\pgfmathsetmacro{\wcl}{(\taus+\taumax)/\slope}
\pgfmathsetmacro{\wext}{1.7}
\begin{axis}[
width=0.9\columnwidth, height=0.6\columnwidth,
axis lines=middle,
xlabel={$\omega$}, ylabel={$\tau$},
xmin=-2.0, xmax=2.0, ymin=-2.0, ymax=2.0,
xtick=\empty, ytick=\empty,
tick style={thick},
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
samples=200,
]
\addplot[name path=upper, thick, blue, domain=-\wcl:\wcl]
{min(\taus - \slope*x, \taumax) - \Bone*x - \Btwo*x*abs(x)};
\addplot[name path=lower, thick, blue, domain=-\wcl:\wcl]
{max(-\taus - \slope*x, -\taumax) - \Bone*x - \Btwo*x*abs(x)};
\addplot[blue, opacity=0.08] fill between[of=upper and lower];
\addplot[thick, dashed, gray, domain=-\wext:\wext]
{-\Bone*x - \Btwo*x*abs(x)};
\node[font=\footnotesize, anchor=north west] at (axis cs: -1.9, -0.5)
{$-b(\omega)$};
\draw[->, gray, thick] (axis cs: -1.5, -0.5) -- (axis cs: -1.3, {0.1 + 0.35*1.3*1.3});
\end{axis}
\end{tikzpicture}
\caption{Nonlinear viscous drag $b(\omega) = B_1\omega + B_2\omega|\omega|$, no friction.}
\label{fig:drag_nonlinear}
\end{subfigure}
\caption{Torque-speed envelopes with mechanical losses. The dashed gray line shows the drag function; the shaded region is the achievable torque at each speed. Note that datasheet torque-speed curves typically plot the first quadrant only.}
\label{fig:drag}
\end{figure}
\paragraph{Rotor Inertia and Gearing.}
Every DC motor datasheet lists the rotor inertia $J_r$ (kg$\cdot$m$^2$). When a gear train with ratio $N$ is attached, the effective inertia reflected to the output shaft is $J_{\text{eff}} = J_r N^2$~\cite{tedrake2024}. Note that real gearboxes also introduce efficiency losses (typically 70--90\%), which reduce the transmitted torque by a multiplicative factor $\eta$, approximated by effectively reducing the motor constant $K_{\text{eff}} = \eta K$.
\paragraph{Cogging Torque.}
Brushless DC motors exhibit \emph{cogging torque}: a position-dependent torque ripple caused by the interaction between permanent magnets and stator slots. It can be modeled as a periodic bias:
\begin{equation}
\tau_{\text{cog}}(\theta) = A \sin(N_p \, \theta + \phi)
\label{eq:cogging}
\end{equation}
where $A$ is the amplitude, $N_p$ is the number of pole pairs times the number of slots per pole, and $\phi$ is a phase offset. Cogging torque is significant primarily at low speeds. Datasheets sometimes report peak cogging as a percentage of rated torque (typically 1--5\%).
\begin{table}[H]
\centering
\footnotesize
\setlength{\tabcolsep}{3pt}
\renewcommand{\arraystretch}{1.2}
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & Formula / Note \\
\midrule
$\tau_c$ & Coulomb friction & $\tau_c\,\text{sgn}(\omega)$ \\
$B$ & Viscous drag (linear) & $-B\,\omega$ \\
$\omega_0$ & No-load speed &
$\omega_0 = v\,K / (K^2 + R\,B)$ \\
$J_r$ & Rotor inertia & units: kg$\cdot$m$^2$ \\
$N$ & Gear ratio & $J_{\text{eff}} = J_r N^2$ \\
$\eta$ & Gearbox efficiency & $K' = \eta \, K$ \\
$A$ & Cogging amplitude & $\tau_{\text{cog}} = A\sin(N_p\theta + \phi)$ \\
$N_p$ & Cogging periodicity & poles $\times$ slots/pole \\
$\phi$ & Cogging phase & offset \\
\bottomrule
\end{tabular}
\caption{Named constants related to mechanical properties. Unlike in Table~\ref{tab:electromech_constants}, the non-approximate expression for $\omega_0$ takes into account the linear drag $B$ (assuming no high-order terms).}
\label{tab:key_constants}
\end{table}
\paragraph{Backlash.}
Gearboxes introduce backlash: a small angular deadband where the motor can turn without moving the output shaft. Datasheets report this in arcminutes. MuJoCo supports backlash modeling via a dual-joint decomposition; \href{https://mujoco.readthedocs.io/en/stable/modeling.html#backlash}{see here} for details.
% ---------------------------------------------------------------------------
% Thermal Model
% ---------------------------------------------------------------------------
\subsection{Thermal Model}
\label{sec:thermal}
Winding temperature affects motor performance primarily through increased copper resistance, and can be modeled as a single lumped thermal state. The thermal constants are
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & Units \\
\midrule
$R_T$ & Thermal resistance & Kelvin/Watt \\
$C$ & Thermal capacitance & Joule/Kelvin \\
$t_T = R_T C$ & Thermal time constant & second \\
$\alpha$ & Resistance temp.\ coefficient & 1/Kelvin \\
$T_0$ & Reference temperature & degree Celsius \\
$T_a$ & Ambient temperature & degree Celsius \\
\bottomrule
\end{tabular}
\caption{Thermal model constants. Units involving temperature differences use Kelvin (equivalent to Celsius for differences); absolute temperatures use degree Celsius, following datasheet convention.}
\end{table}
\noindent Note that some manufacturers specify two thermal resistances: $R_{\text{th1}}$ (winding-to-housing) and $R_{\text{th2}}$ (housing-to-ambient), which sum to give the total winding-to-ambient thermal resistance $R_T = R_{\text{th1}} + R_{\text{th2}}$. The single-node model above uses $R_T$ directly.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Lumped Thermal ODE}
\label{sec:thermal_ode}
The winding temperature $T$ evolves according to a first-order lumped model driven by the power dissipation $P$ (Watt, detailed in \S\ref{sec:thermal_losses}):
\begin{equation}
\frac{\partial T}{\partial t} = \frac{1}{C} P - \frac{T - T_a}{t_T}
\label{eq:thermal_ode}
\end{equation}
where $t_T = R_T C$ is the thermal time constant. This produces exponential rise/decay toward a steady-state temperature $T_{ss} = T_a + R_T P$ (Figure~\ref{fig:thermal_response}).
\begin{figure}[ht]
\centering
\resizebox{\columnwidth}{!}{%
\begin{tikzpicture}
\pgfmathsetmacro{\Tss}{1.0}
\pgfmathsetmacro{\ttau}{1.0}
\pgfmathsetmacro{\xmax}{4.5}
\begin{axis}[
width=0.9\columnwidth, height=0.45\columnwidth,
axis lines=left,
clip=false,
xlabel={$t$}, ylabel={$T - T_a$},
xmin=0, xmax=\xmax, ymin=0, ymax={\Tss*1.25},
xtick={\ttau}, xticklabels={$t_T$},
ytick={{\Tss*(1-exp(-1))}, \Tss},
yticklabels={$(1{-}1/e)\,R_T P$, $R_T P$},
tick style={thick},
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
]
\addplot[thick, blue, domain=0:\xmax, samples=100]
{\Tss*(1 - exp(-x/\ttau))};
\addplot[thick, dashed, gray] coordinates {(0,\Tss) (\xmax,\Tss)};
\draw[thick, dashed, gray] (axis cs:\ttau, 0) -- (axis cs:\ttau, {\Tss*(1-exp(-1))});
\draw[thick, dashed, gray] (axis cs:0, {\Tss*(1-exp(-1))}) -- (axis cs:\ttau, {\Tss*(1-exp(-1))});
\node[font=\scriptsize, anchor=south] at (axis cs:\xmax*0.5, \Tss)
{$T_{ss} = T_a + R_T P$};
\end{axis}
\end{tikzpicture}}%
\caption{Temperature rise under constant power dissipation $P$.
At $t = t_T$, it reaches $(1-1/e) \approx 63\%$ of its steady-state value.}
\label{fig:thermal_response}
\end{figure}
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Losses}
\label{sec:thermal_losses}
The dominant loss is copper (Joule) heating:
\begin{equation*}
P = i^2 R(T)
\label{eq:copper_loss}
\end{equation*}
Optionally, speed-dependent iron losses (eddy-current and hysteresis losses in the stator laminations) can be included~\cite{hughes2019}:
\begin{equation*}
P = i^2 R(T) + K_{\text{fe}} \omega^2
\label{eq:total_loss}
\end{equation*}
The iron loss coefficient $K_{\text{fe}}$ is not typically listed on datasheets and must be identified from efficiency curves or manufacturer simulation tools. For most hobby and robotics motors, iron losses are small compared to copper losses and can be neglected. They become significant at high speeds in large industrial motors.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Temperature-Dependent Resistance}
\label{sec:resistance_temperature}
Copper resistance increases approximately linearly with temperature:
\begin{equation}
R(T) = R_0 \left(1 + \alpha (T - T_0)\right)
\label{eq:resistance_temperature}
\end{equation}
where $R_0$ is resistance at reference temperature $T_0$ and $\alpha \approx 0.0039 \, \text{K}^{-1}$ for copper. This is the dominant thermal feedback: as $T$ rises, $R$ increases, so for a given voltage the current $i = (v - K \omega) / R(T)$ drops, reducing torque.
Note that $R(T)$ also increases heating for a given current ($P = i^2 R(T)$), creating mild positive feedback under current control.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Magnet Flux Derating}
\label{sec:magnet_derating}
Permanent magnet flux weakens with temperature, reducing $K$:
\begin{equation*}
K(T) = K_0 \left(1 + \alpha_m (T - T_0)\right)
\label{eq:kt_temperature}
\end{equation*}
with $\alpha_m < 0$ (motor-dependent). This effect is often small over normal operating ranges and can be ignored.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Thermal Derating}
\label{sec:thermal_derating}
Real actuators limit current as winding temperature approaches a maximum:
\begin{equation*}
i_{\max}(T) = \begin{cases}
i_{\text{rated}} & T \le T_1 \\
i_{\text{safe}} + (i_{\text{rated}} - i_{\text{safe}}) \, s(T) & T_1 < T < T_2 \\
i_{\text{safe}} & T \ge T_2
\end{cases}
\end{equation*}
where $s(T)$ is a smooth interpolant between $T_1$ and $T_2$. This reduces the maximum available torque as the motor heats up.
% ---------------------------------------------------------------------------
% Micro-Friction Models
% ---------------------------------------------------------------------------
\subsection{Micro-Friction Models}
\label{sec:micro_friction}
Simple macroscopic friction models (Coulomb, viscous) cannot capture complex mechanical phenomena common in real motors with gear trains, such as pre-sliding hysteresis and stick-slip limit cycles. To capture these behaviors, a richer dynamic model is required. At the microscopic level, two surfaces in contact touch at many asperities which deform elastically under tangential load. This can be modeled as an average bristle deflection $z$, governed by a first-order ODE driven by the relative velocity $\omega$. Friction torque is then a function of $z$, $\dot{z}$, and $\omega$.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Dahl Model}
\label{sec:dahl}
The simplest stateful model~\cite{dahl68} treats friction as a rate-independent hysteresis operator derived from the stress-strain curve:
\begin{equation*}
\dot{z} = \omega - \frac{\sigma_0}{\tau_c} |\omega| \, z
\label{eq:dahl_state}
\end{equation*}
with output $\tau = -\sigma_0 z$. In steady state ($\dot{z}=0$), $z_{ss} = \tau_c \, \text{sgn}(\omega) / \sigma_0$ so $\tau_{ss} = -\tau_c \, \text{sgn}(\omega)$: pure Coulomb friction opposing motion. The two parameters are the bristle stiffness $\sigma_0$ (torque/radian) and the Coulomb friction torque $\tau_c$.
For small displacements the model is approximately linear ($\tau \approx -\sigma_0 \theta$), giving spring-like pre-sliding behavior with hysteresis during direction reversals (Figure~\ref{fig:hysteresis}). The Dahl model does not capture the Stribeck effect~\cite{stribeck1902} (the drop in friction at low velocity) and thus cannot predict stick-slip motion.
\begin{figure}[H]
\centering
\begin{tikzpicture}
\begin{axis}[
width=0.9\columnwidth, height=0.55\columnwidth,
axis lines=middle,
xlabel={$\theta$}, ylabel={$\tau$},
xmin=-1.4, xmax=1.4, ymin=-1.4, ymax=1.4,
xtick=\empty, ytick=\empty,
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
clip=false,
]
\pgfmathsetmacro{\sig}{2.5}
\pgfmathsetmacro{\Fc}{1.0}
\pgfmathsetmacro{\xm}{1.0}
\pgfmathsetmacro{\ch}{(exp(\sig*\xm)+exp(-\sig*\xm))/2}
\addplot[thick, blue, domain=-\xm:\xm, samples=150, name path=lower]
{-\Fc*(1 - exp(-\sig*x)/\ch)};
\addplot[thick, blue, domain=-\xm:\xm, samples=150, name path=upper]
{\Fc*(1 - exp(\sig*x)/\ch)};
\addplot[blue, opacity=0.08] fill between[of=lower and upper];
\draw[thick, dotted] (axis cs:-1.4, -\Fc) -- (axis cs:1.4, -\Fc)
node[right, font=\scriptsize] {$-\tau_c$};
\draw[thick, dotted] (axis cs:-1.4, \Fc) -- (axis cs:1.4, \Fc)
node[right, font=\scriptsize] {$\tau_c$};
\draw[->, thick, gray] (axis cs:0.05, -0.78) -- (axis cs:0.25, -0.83);
\draw[->, thick, gray] (axis cs:-0.05, 0.78) -- (axis cs:-0.25, 0.83);
\end{axis}
\end{tikzpicture}
\caption{Hysteresis loop: friction torque $\tau$ vs.\ displacement $\theta$ under slow
periodic loading (Dahl model).
The loop area represents energy dissipated per cycle.
Unlike memoryless Coulomb friction, the torque is continuous.}
\label{fig:hysteresis}
\end{figure}
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{LuGre Model}
\label{sec:lugre}
The LuGre model~\cite{dewit95, lugre_revisited} extends Dahl by making the bristle saturation velocity-dependent and adding micro-damping and viscous terms:
\begin{subequations}
\label{eq:lugre}
\begin{align}
\dot{z} &= \omega - \sigma_0 \frac{|\omega|}{g(\omega)} z
\label{eq:lugre_state} \\
\tau &= -(\sigma_0 z + \sigma_1 \dot{z} + \sigma_2 \omega)
\label{eq:lugre_force}
\end{align}
\end{subequations}
where the function $g(\omega)$ captures the Stribeck effect:
\begin{equation}
g(\omega) = \tau_c + (\tau_s - \tau_c) \, e^{-(\omega/\omega_s)^\gamma}
\label{eq:stribeck}
\end{equation}
with exponent $\gamma$ typically 1 or 2. In steady state, $\tau_{ss}(\omega) = -g(\omega) \, \text{sgn}(\omega) - \sigma_2 \omega$: the classic Stribeck curve (Figure~\ref{fig:stribeck}).
\begin{figure}[H]
\centering
\begin{tikzpicture}
\begin{axis}[
width=0.9\columnwidth, height=0.55\columnwidth,
axis lines=middle,
xlabel={$\omega$}, ylabel={$\tau_{ss}$},
xmin=-3, xmax=3, ymin=-2.5, ymax=2.5,
xtick=\empty, ytick=\empty,
every axis x label/.style={at={(ticklabel* cs:1)}, anchor=west},
every axis y label/.style={at={(ticklabel* cs:1)}, anchor=south},
clip=false,
]
\pgfmathsetmacro{\Fc}{1.0}
\pgfmathsetmacro{\Fs}{1.8}
\pgfmathsetmacro{\vs}{0.5}
\pgfmathsetmacro{\sigtwo}{0.15}
\addplot[thick, blue, domain=0.01:3, samples=200]
{-(\Fc + (\Fs-\Fc)*exp(-(x/\vs)^2)) - \sigtwo*x};
\addplot[thick, blue, domain=-3:-0.01, samples=200]
{(\Fc + (\Fs-\Fc)*exp(-(-x/\vs)^2)) - \sigtwo*x};
\addplot[thick, dashed, gray, domain=-3:3, samples=2] {-\sigtwo*x};
\draw[thick, dotted] (axis cs:0,-\Fs) -- (axis cs:3,-\Fs)
node[right, font=\scriptsize] {$-\tau_s$};
\draw[thick, dotted] (axis cs:0,-\Fc) -- (axis cs:3,-\Fc)
node[right, font=\scriptsize] {$-\tau_c$};
\draw[thick, dotted] (axis cs:0,\Fs) -- (axis cs:-3,\Fs)
node[left, font=\scriptsize] {$\tau_s$};
\draw[thick, dotted] (axis cs:0,\Fc) -- (axis cs:-3,\Fc)
node[left, font=\scriptsize] {$\tau_c$};
\node[font=\footnotesize, anchor=north east] at (axis cs:-0.5, -0.1)
{$-\sigma_2\omega$};
\end{axis}
\end{tikzpicture}
\caption{Steady-state friction $\tau_{ss}(\omega) = -g(\omega)\,\text{sgn}(\omega) - \sigma_2 \omega$.
Stiction torque $\tau_s$ at $\omega\!=\!0$ drops to Coulomb level $\tau_c$
over velocity scale $\omega_s$ (Stribeck effect). Friction opposes motion.}
\label{fig:stribeck}
\end{figure}
\noindent The Dahl model is recovered by setting $g(\omega) = \tau_c$ and $\sigma_1 = \sigma_2 = 0$. Linearizing around $\omega = z = 0$ gives second-order dynamics $J\ddot{\theta} - (\sigma_1 + \sigma_2)\dot{\theta} - \sigma_0 \theta = \tau$ (applied torque): a spring-damper with natural frequency $\omega_n = \sqrt{\sigma_0/J}$, critically damped when $\sigma_1 = 2\sqrt{J\sigma_0}$.
The LuGre model can be shown to be input-strictly-passive (the map $\omega \mapsto \tau$ dissipates energy) provided $\sigma_2 > \sigma_1 (\tau_s - \tau_c)/\tau_c$. This passivity condition limits $\sigma_1$ and can lead to underdamped micro-dynamics, motivating the following extension.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Velocity-Dependent Damping}
\label{sec:vel_damping}
The passivity constraint on $\sigma_1$ can be relaxed by making the micro-damping decrease with velocity:
\begin{equation*}
\sigma_1(\omega) = \bar{\sigma}_1\, e^{-(\omega/\omega_s)^\beta}
\label{eq:sigma1_vel}
\end{equation*}
This allows large damping in the stiction regime (good for numerical stability and physical fidelity) while satisfying passivity at higher velocities where $\sigma_1 \to 0$.
Together with $\tau_c$ from \S\ref{sec:mechanical}, the LuGre model with velocity-dependent damping adds six parameters:
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & Units \\
\midrule
$\sigma_0$ & Bristle stiffness, pre-sliding slope & N$\cdot$m/rad \\
$\bar{\sigma}_1$ & Peak bristle damping at $\omega = 0$ & N$\cdot$m$\cdot$s/rad \\
$\sigma_2$ & Viscous damping coefficient & N$\cdot$m$\cdot$s/rad \\
$\tau_s$ & Stiction torque, $\tau_s \ge \tau_c$ & N$\cdot$m \\
$\omega_s$ & Stribeck velocity & rad/s \\
$\beta$ & Damping decay exponent & dimensionless \\
\bottomrule
\end{tabular}
\caption{Parameters of the LuGre friction model (\S\ref{sec:lugre}--\ref{sec:vel_damping}).}
\label{tab:lugre_params}
\end{table}
\newpage
%=============================================================================
% IMPLEMENTATION
%=============================================================================
\section{Implementation}
\label{sec:implementation}
Here we describe MuJoCo's \href{https://mujoco.readthedocs.io/en/stable/XMLreference.html#actuator-dcmotor}{\texttt{dcmotor} actuator}. Some scalars are grouped into vectors; we use a colon to denote such scalar sub-attributes, e.g.\ \texttt{cogging:phase} refers to the third element of the \texttt{cogging} attribute (see Tables \ref{tab:mjcf_attributes} and \ref{tab:cogging_impl}).
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}llp{5cm}@{}}
\toprule
Attribute & Size & Description \\
\midrule
\texttt{resistance} & 1 & Terminal resistance $R$ \\
\texttt{motorconst} & 2 & Motor constants ($K_t, K_e$; see below) \\
\texttt{nominal} & 3 & Nominal operating point ($v_n, \tau_0, \omega_0$) \\
\texttt{inductance} & 2 & Electrical dynamics ($L, t_e$) \\
\texttt{thermal} & 6 & Thermal model ($R_T, C, t_T, \alpha, T_0, T_a$) \\
\texttt{saturation} & 3 & Limits ($\tau_{\max}, i_{\max}, (di{/}dt)_{\max}$) \\
\midrule
\texttt{cogging} & 3 & Cogging torque ($A, N_p, \phi$) \\
\texttt{lugre} & 5 & LuGre friction ($\sigma_0, \sigma_1, \tau_c, \tau_s, \omega_s$) \\
\texttt{damping} & 3 & Viscous damping coefficients \\
\texttt{armature} & 1 & Armature inertia \\
\midrule
\texttt{input} & string & Input block: subset of \texttt{pos vel ff voltage}, or \texttt{none} \\
\texttt{controller} & 6 & Gains, slew, and voltage saturation ($k_p, k_i, k_d, s, I_{\max}, v_{\max}$) \\
\bottomrule
\end{tabular}
\caption{MJCF attributes for the \texttt{dcmotor} actuator, split into electrical, mechanical and controller groupings.}
\label{tab:mjcf_attributes}
\end{table}
% ---------------------------------------------------------------------------
% Stateless dcmotor
% ---------------------------------------------------------------------------
\subsection{Stateless DC Motor}
\label{sec:dcmotor}
The output torque of the stateless motor follows Eq.~\eqref{eq:saturation}, mapping physical parameters to the underlying affine model. The three core parameters are the effective motor constant $K$, resistance $R$, and maximum torque $\tau_{\max}$.
They are stored in \texttt{mjModel} as follows:
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & Description & \texttt{mjModel} storage \\
\midrule
$R$ & Resistance & \texttt{gainprm[0]} \\
$K$ & Effective motor constant & \texttt{gainprm[1]} \\
$\tau_{\max}$ & Maximum torque & \texttt{forcerange} \\
\bottomrule
\end{tabular}
\caption{Stateless DC motor core parameters.}
\end{table}
\noindent The gain $G = K/R$ and back-EMF bias $-GK\omega$ are computed at runtime. Storing $R$ separately allows temperature-dependent resistance $R(T)$, Eq.~\eqref{eq:resistance_temperature}, to be applied. These three core parameters can be specified with a combination of eight sub-attributes
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atR{} & $R$ & Ohm \\
\atKt{} & $K_t$ & N$\cdot$m/A \\
\atKe{} & $K_e$ & V$\cdot$s/rad \\
\atVM{} & $v_n$ & Volt \\
\atSTALL{} & $\tau_0\!=\!Kv_n/R$ & N$\cdot$m \\
\atNLS{} & $\omega_0\!\approx\!v_n/K$ & rad/s \\
\atTMAX{} & $\tau_{\max}$ & N$\cdot$m \\
\atIMAX{} & $i_{\max}$ & Ampere \\
\bottomrule
\end{tabular}
\caption{Stateless DC motor basic attributes.}
\end{table}
\noindent The attribute \atK{} has two sub-attributes, \texttt{Kt} and \texttt{Ke}. If both are positive, $K = \sqrt{K_t K_e}$ (preserving power balance $K^2 = K_t K_e$). If only one is positive, $K$ equals that value.
\pagebreak
\noindent The following attribute combinations are supported:
\begin{enumerate}[itemsep=2pt, parsep=0pt, topsep=2pt]
\item Effective motor constant $K$, one of:
\begin{itemize}[itemsep=1pt, parsep=0pt, topsep=1pt]
\item \atKt{} \emph{and/or} \atKe{}
\item \atNLS{} \emph{and} \atVM{}
\end{itemize}
\item Resistance $R$, one of:
\begin{itemize}[itemsep=1pt, parsep=0pt, topsep=1pt]
\item \atR{}
\item \atSTALL{} \emph{and} \atVM{}
\end{itemize}
\item Maximum torque $\tau_{\max}$, one of:
\begin{itemize}[itemsep=1pt, parsep=0pt, topsep=1pt]
\item \atTMAX{}
\item \atIMAX{}
\end{itemize}
\end{enumerate}
\noindent \atIMAX{} corresponds to the continuous (thermal) current limit. Peak current behavior can be approximated with the thermal model (\S\ref{sec:temperature_impl}).
\paragraph{Rotor Inertia and Gearing.} To model rotor inertia with a gear train, set the actuator's \texttt{armature} $= J_r$ and \texttt{gear} $= N$. Actuator-level \texttt{armature} automatically scales the inertia by $N^2$ to reflect $J_{\text{eff}}$ to the output shaft.
\paragraph{Mechanical Drag.} The full torque-speed envelope applies viscous drag \emph{outside} the current clamp:
\begin{equation*}
\tau_{\text{net}} = \text{clip}\!\left( \frac{K}{R}(v - K \, \omega),\;
\pm \tau_{\max} \right) - b(\omega)
\label{eq:drag}
\end{equation*}
Actuator-level \texttt{damping} reproduces this post-clamp behavior. It accepts an array of polynomial drag coefficients (\texttt{damping[0]} $= B_1$, \texttt{damping[1]} $= B_2$, $\dots$). As with \texttt{armature}, actuator-level \texttt{damping} is scaled by $N^2$.
\paragraph{Cogging Torque.} The magnetic torque ripple of Eq.~\eqref{eq:cogging} is modeled as a periodic bias added to the actuator force, where $\theta$ is the \texttt{actuator\_length}, i.e.\ the transmission-transformed joint angle.
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atCOGA{} & $A$ & N$\cdot$m \\
\atCOGP{} & $N_p$ & dimensionless \\
\atCOGPH{} & $\phi$ & radian \\
\bottomrule
\end{tabular}
\caption{Cogging torque attributes.}
\label{tab:cogging_impl}
\end{table}
\paragraph{Mapping to Isaac Lab.}
Isaac Lab~\cite{isaaclab2025} implements the stateless DC motor model. Table~\ref{tab:mapping} maps its attributes to the constants defined in this document and to the \texttt{dcmotor} attributes.
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}lll@{}}
\toprule
Symbol & MuJoCo & Isaac Lab \\
\midrule
$\tau_0$ & \atSTALL{} & \texttt{saturation\_effort} \\
$\omega_0$ & \atNLS{} & \texttt{velocity\_limit} \\
$\tau_{\max}$ & \atTMAX{} & \texttt{effort\_limit} \\
\bottomrule
\end{tabular}
\caption{Mapping of attributes to Isaac Lab.}
\label{tab:mapping}
\end{table}
\noindent Isaac Lab does not expose electrical parameters. To reproduce the same torque-speed envelope, set \atVM{} to any positive value (e.g.,~\texttt{1}) and \texttt{ctrlrange} to $\pm$\,that value (e.g., \texttt{"-1 1"}).
\paragraph{Gearbox Efficiency.} Gearbox efficiency $\eta$ is not a separate attribute. To account for transmission losses, reduce the motor constant: $K \rightarrow \eta K$. This correctly reduces forward torque transmission.
\paragraph{Computed parameters.}
Several derived quantities that appear on datasheets can be computed and used to cross-check the parameterization. The torque-speed gradient $\partial\omega/\partial\tau = -R/K^2$ gives the slope of the torque-speed line (Table~\ref{tab:electromech_constants}). The mechanical time constant $t_m = R\,J/K^2$ is the time for the motor to reach 63\% of its no-load speed under a voltage step, where $J$ is the rotor inertia (\texttt{armature}). The nominal (continuous) torque is $\tau_n = K \cdot i_{\max}$. The no-load current $i_0$ can be computed from Eq.~\eqref{eq:noload} given known friction parameters. See Table~\ref{tab:datasheet}.
\paragraph{Not modeled:}
Nonlinear torque constant $K_t(i)$. Separate $K_t$ and $K_e$ values are accepted via \atKt{} and \atKe{} but collapsed to a single effective $K = \sqrt{K_t K_e}$.
% ---------------------------------------------------------------------------
% Stateful Current
% ---------------------------------------------------------------------------
\subsection{Stateful Current}
\label{sec:current_impl}
A winding current state variable governed by Eq.~\eqref{eq:inductance} is added if the electrical time constant $t_e > 0$ (derived from inductance $L > 0$ or specified directly). When enabled, the state is integrated by \texttt{mjDYN\_DCMOTOR}, and the gain switches from $K/R$ (stateless) to $K$ (stateful).
The time constant $t_e$ can be determined by either:
\begin{itemize}[itemsep=0pt, parsep=0pt, topsep=2pt]
\item \atTE{}
\item \atL{} \emph{and} \atR{} (via $t_e = L/R$)
\end{itemize}
\paragraph{Current rate limiting.} When the sub-attribute \atCRATE{} is set ($(di/dt)_{\max} > 0$) and the current state is enabled ($t_e > 0$), the rate of change of current is clamped:
\begin{equation*}
\frac{di}{dt} \leftarrow \text{clip}\!\left(\frac{di}{dt},\; \pm(di/dt)_{\max}\right)
\end{equation*}
This limits the torque ramp rate to $K \cdot (di/dt)_{\max}$ (N$\cdot$m/s) without requiring any additional state variables, since the current $i$ is already an activation variable and we are simply clamping its rate of change. This attribute has no effect when $t_e = 0$ (stateless current).
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atL{} & $L$ & Henry \\
\atTE{} & $t_e\!=\!L/R$ & second \\
\atCRATE{} & $(di{/}dt)_{\max}$ & Ampere/second \\
\bottomrule
\end{tabular}
\caption{Stateful current attributes.}
\end{table}
% ---------------------------------------------------------------------------
% Temperature
% ---------------------------------------------------------------------------
\subsection{Temperature}
\label{sec:temperature_impl}
A winding temperature state governed by the lumped ODE~\eqref{eq:thermal_ode} is added if any of the thermal attributes ($R_T, C, t_T$) are specified. The state $T$ is the temperature rise above ambient ($T = T_{\text{winding}} - T_a$), so the absolute temperature is $T + T_a$. Temperature modifies the winding resistance via Eq.~\eqref{eq:resistance_temperature}, which feeds back into the motor equation: higher temperature increases resistance, leading to reduced current for a given voltage.
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atRT{} & $R_T$ & K/W \\
\atTC{} & $C$ & J/K \\
\atTT{} & $t_T\!=\!R_T C$ & s \\
\atALPHA{} & $\alpha$ & 1/K \\
\atTREF{} & $T_0$ & \textdegree C \\
\atTAMB{} & $T_a$ & \textdegree C \\
\bottomrule
\end{tabular}
\caption{Thermal model attributes.}
\end{table}
\noindent The time constant $t_T$ can be determined by either:
\begin{itemize}[itemsep=0pt, parsep=0pt, topsep=2pt]
\item \atTT{}
\item \atRT{} \emph{and} \atTC{}
\end{itemize}
\paragraph{Not modeled:}
Iron losses (\S\ref{sec:thermal_losses}), magnet flux derating (\S\ref{sec:magnet_derating}), and thermal current derating (\S\ref{sec:thermal_derating}). Only copper losses ($i^2 R$) drive the thermal model; $K$ is treated as temperature-independent.
% ---------------------------------------------------------------------------
% Stateful Friction
% ---------------------------------------------------------------------------
\subsection{Stateful Friction}
\label{sec:friction_impl}
A bristle deflection state governed by the LuGre model (\S\ref{sec:lugre}) is added if the bristle stiffness $\sigma_0 > 0$.
The Stribeck function $g(\omega)$, Eq.~\eqref{eq:stribeck}, determines velocity-dependent friction, and the friction force is given by Eq.~\eqref{eq:lugre_force}. The bristle state is integrated using the exact ZOH scheme~\eqref{eq:zoh}. The viscous term $\sigma_2 \omega$ is specified by via the standard \texttt{damping} attribute to leverage MuJoCo's implicit integration.
\paragraph{Integration.}
The bristle stiffness $\sigma_0$ is typically very large ($10^5$--$10^6$ N$\cdot$m/rad), creating a stiff ODE. At constant velocity, the state equation~\eqref{eq:lugre_state} has the form $\dot{z} = a z + b \omega$ where $a = -\sigma_0 |\omega| / g(\omega)$ and $b = 1$. Euler integration is unstable unless $|1 + a \Delta t| < 1$, requiring impractically small timesteps ($\Delta t < 2g(\omega)/(\sigma_0 |\omega|)$, on the order of microseconds).
Under a zero-order hold assumption ($\omega$ constant over the timestep), the linear ODE $\dot{z} = az + b\omega$ can be solved exactly:
\begin{equation}
z_{k+1} = e^{a \Delta t} z_k
+ \frac{b(e^{a \Delta t} - 1)}{a} \, \omega
\label{eq:zoh}
\end{equation}
reducing to $z_{k+1} = z_k + b\omega\Delta t$ in the limit $a \to 0$. This integration is unconditionally stable for any $\Delta t$.
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atSIG{} & $\sigma_0$ & N$\cdot$m/rad \\
\atSIGD{} & $\sigma_1$ & N$\cdot$m$\cdot$s/rad \\
\atTAUC{} & $\tau_c$ & N$\cdot$m \\
\atTAUS{} & $\tau_s$ & N$\cdot$m \\
\atWS{} & $\omega_s$ & rad/s \\
\bottomrule
\end{tabular}
\caption{LuGre friction attributes.}
\end{table}
\noindent\textbf{Not modeled:}
Velocity-dependent bristle damping $\sigma_1(\omega)$ (\S\ref{sec:vel_damping}), a constant $\sigma_1$ is used. The Stribeck exponent is not exposed and fixed at $\gamma = 2$.
\newpage
% ---------------------------------------------------------------------------
% PID Controller
% ---------------------------------------------------------------------------
\subsection{PID Controller}
\label{sec:controller}
Many actuators embed an on-board controller computing drive voltage from position or velocity commands. To model such actuators, \texttt{dcmotor} supports an optional controller layer upstream of the motor physics. Two attributes control this behavior:
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}llp{4.2cm}@{}}
\toprule
Attribute & Type & Description \\
\midrule
\texttt{input} & string & subset of \texttt{pos vel ff voltage}, in canonical order; \texttt{none} (passive) \\
\texttt{controller} & vector & Gains, slew, and voltage saturation ($k_p, k_i, k_d, s, I_{\max}, v_{\max}$) \\
\bottomrule
\end{tabular}
\caption{Controller attributes. Default \texttt{input} is \texttt{voltage}.}
\label{tab:controller_attributes}
\end{table}
\noindent Unlike the motor parameters in Table~\ref{tab:datasheet}, controller gains are user-specified firmware settings. MuJoCo uses {\em torque-space} units (e.g., $k_p$ in N$\cdot$m/rad), matching the \texttt{pid} actuator. Voltage-space datasheet gains (V/rad) convert by multiplying by $K/R$, though empirical calibration is often necessary due to unknown internal units on real hardware.
The controller computes a target voltage $v$ from the \texttt{ctrl} command. All motor physics --- cogging, saturation, friction, etc. --- apply identically downstream of $v$. The \texttt{input} attribute selects the controller:
\begin{figure}[H]
\centering
\resizebox{\columnwidth}{!}{%
\begin{tikzpicture}[
block/.style={draw, rounded corners=2pt, minimum height=1.6em,
font=\scriptsize, fill=blue!5},
mode/.style={font=\scriptsize, text=blue!70!black},
arr/.style={->, thick, >=stealth},
every node/.style={inner sep=2pt},
]
% ctrl input
\node[font=\small] (ctrl) at (0, 3.8) {\texttt{ctrl} $=$ Input block $(\theta^*, \dot\theta^*, \tau_{f\!f}, v_{\text{raw}})$ \; (present members only)};
% Controller box
\node[block, minimum width=7.2cm, minimum height=6.5em, align=center]
(sel) at (0, 1.8) {};
\node[font=\footnotesize\bfseries, anchor=north] at (0, 2.7)
{Controller};
\node[mode] at (0, 1.45) {$\begin{aligned}
\tau &= k_p(\theta^*\!-\!\theta) + k_d(\dot\theta^*\!-\!\dot\theta) + k_i x_I + \tau_{f\!f} \\[2pt]
v &= \text{clip}(\tfrac{R}{K}\tau + K\dot\theta,\, \pm v_{\max}) + v_{\text{raw}} \\[2pt]
&\phantom{=}\ \text{absent inputs are zero;}\quad \text{\texttt{input=\textquotedbl none\textquotedbl:}}\ v = 0
\end{aligned}$};
% arrow ctrl to controller
\draw[arr] (ctrl.south) -- (sel.north);
% Motor block
\node[block, font=\footnotesize\bfseries, minimum width=5.5cm, minimum height=2.2em, align=center]
(motor) at (0, -0.8) {DC Motor physics};
% single arrow with v label
\draw[arr] (sel.south) -- (motor.north)
node[midway, fill=white, font=\small, inner sep=2pt] {Voltage $v$};
% output
\node[font=\small] (tau) at (0, -1.8) {Torque $\tau$};
\draw[arr] (motor.south) -- (tau.north);
\end{tikzpicture}}%
\caption{Controller pipeline. The \texttt{input} attribute selects which members of the control block are present; motor physics is identical downstream.}
\label{fig:controller_pipeline}
\end{figure}
\subsubsection{Setpoint Mode}
\label{sec:setpoint_mode}
When \texttt{input="pos vel ff"} (or subset thereof), the actuator owns a
block of controls $u = (\theta^*, \dot\theta^*, \tau_{f\!f})$: a position setpoint, a velocity
setpoint and a torque feedforward. Any subset may be selected, and the \texttt{dcmotor}'s \texttt{actuator\_ctrlnum} is variable.
An absent input corresponds to a setpoint frozen at
zero. The controller commands a torque and converts it to drive voltage:
\begin{align}
\tau &= k_p \, (\theta^* - \theta) + k_d \, (\dot\theta^* - \dot\theta)
+ k_i \, x_I \; + \, \tau_{f\!f}
\label{eq:setpoint_mode} \\
v &= \frac{R}{K} \, \tau + K \dot\theta
\label{eq:torque_to_voltage}
\end{align}
where the second term of \eqref{eq:torque_to_voltage} compensates back-EMF, as the
current loop of a real torque-mode driver does: commanded torque is delivered {\em
exactly} until a limit binds, and the torque-speed envelope emerges from voltage
saturation (\S\ref{sec:voltage_saturation}). The map uses the nameplate $R$; thermal
resistance growth (\S\ref{sec:resistance_temperature}) is not compensated. The
integrator state accumulates the position error, $\dot x_I = \theta^* - \theta$,
subject to anti-windup (\S\ref{sec:anti_windup}). When $k_i = 0$, no integral state
is added and the controller reduces to PD; when $k_p = k_i = 0$, it is a pure
velocity servo. The back-EMF compensation also cancels the motor's $-K^2/R$ damping
bias, so the net physical damping delivered by an unclipped torque-mode motor is
$-k_d$, matching \texttt{pid}. The separate \texttt{voltage} input is the raw
terminal voltage, added downstream of the controller and its saturation.
\subsubsection{Passive Operation}
\label{sec:passive}
\texttt{input="none"} selects the {\em empty} input block: the actuator owns no
controls at all and acts as a purely passive device. The terminal voltage is
identically zero, so the motor operates with shorted terminals: back-EMF drives
current through $R$ and brakes the joint (dynamic braking), while cogging
\eqref{eq:cogging} and LuGre friction (\S\ref{sec:lugre}) act as passive joint
forces. Setting \texttt{motorconst} to zero decouples the electrical branch,
leaving pure friction and cogging. This exists because MuJoCo associates auxiliary
dynamic states (such as the LuGre bristle) with actuators, not joints: a passive
\texttt{dcmotor} is the idiomatic way to attach these passive phenomena to a joint.
Controller gains, slew rate limiting and integral gain require a controller input and are rejected.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Setpoint Slew Rate}
\label{sec:setpoint_slew}
The user command $u = {}$\texttt{ctrl} can change discontinuously between timesteps. When \atSLEW{} is set ($s > 0$), the effective setpoint is rate-limited:
\begin{equation*}
u \leftarrow \text{clip}(u, \; u_{\text{prev}} \pm s \cdot \Delta t)
\end{equation*}
where $u_{\text{prev}}$ is the previous effective setpoint and $\Delta t$ is the timestep. This smoothly ramps the reference trajectory instead of allowing instantaneous jumps.
\paragraph{State variable.} When $s > 0$, one activation state $u_{\text{prev}}$ is allocated, and updated each step to $u$ (post-clamping).
\paragraph{Units.} The slew rate $s$ limits the first controller input, with corresponding units: position setpoint (rad/s), velocity setpoint (rad/s$^2$), or torque feedforward (N$\cdot$m/s). The raw \texttt{voltage} input is never rate-limited; \texttt{slewmax} requires a controller input.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Anti-windup}
\label{sec:anti_windup}
When $k_i > 0$, the integrator state $x_I$ provides steady-state error correction. However, sustained saturation or large setpoint changes can cause $x_I$ to grow excessively, leading to overshoot (integral windup). To prevent this, when \atIMAXINT{} is set ($I_{\max} > 0$), the state is bounded each step:
\begin{equation*}
x_I \leftarrow \text{clip}(x_I, \pm I_{\max})
\end{equation*}
This prevents controller windup even when drive signals are saturated.
% - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
\subsubsection{Voltage Saturation}
\label{sec:voltage_saturation}
With setpoint inputs, the computed voltage $v$ can be arbitrarily large (proportional to the error). Real motor drivers are limited by their supply voltage. When \atVMAX{} is set ($v_{\max} > 0$), a voltage clamp is applied before the motor equations:
\begin{equation*}
v \leftarrow \text{clip}(v, \pm v_{\max})
\label{eq:vlimit}
\end{equation*}
This differs from \texttt{ctrlrange} (clamping user commands $u$) and \texttt{forcerange} (clamping output torque). The clamp applies to the controller output only: the raw \texttt{voltage} input is added downstream, unclamped (use \texttt{ctrlrange} to bound it). Because the torque-to-voltage map \eqref{eq:torque_to_voltage} compensates back-EMF, the achievable torque under \atVMAX{} shrinks with speed: the torque-speed envelope of \S\ref{sec:electromagnetics}.
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lll@{}}
\toprule
Attribute & Symbol & Units \\
\midrule
\atKP{} & $k_p$ & N$\cdot$m/rad \\
\atKI{} & $k_i$ & N$\cdot$m/(rad$\cdot$s) \\
\atKD{} & $k_d$ & N$\cdot$m$\cdot$s/rad \\
\atSLEW{} & $s$ & ctrl-units/s \\
\atIMAXINT{} & $I_{\max}$ & rad$\cdot$s \\
\atVMAX{} & $v_{\max}$ & Volt \\
\bottomrule
\end{tabular}
\caption{Controller attributes.}
\label{tab:controller_attrs}
\end{table}
% ---------------------------------------------------------------------------
\vfill\newpage
% Low-Level Semantics
% ---------------------------------------------------------------------------
\subsection{Low-Level Semantics}
\label{sec:array_semantics}
The \texttt{dcmotor} actuator uses the enum value types \texttt{mjGAIN\_DCMOTOR}, \texttt{mjDYN\_DCMOTOR}, \texttt{mjBIAS\_DCMOTOR}, and populates the rows of several \texttt{mjModel} arrays (all \texttt{actuator\_*}), as follows:
\begin{table}[H]
\centering
\footnotesize
\begin{tabular}{@{}llll@{}}
\toprule
Array & Index & Symbol & Description \\
\midrule
\texttt{gainprm} & 0 & $R$ & Resistance ($\Omega$) \\
& 1 & $K$ & Motor constant (N$\cdot$m/A) \\
& 2 & $\alpha$ & Resistance coeff.\ ($\text{K}^{-1}$) \\
& 3 & $T_0$ & Reference temperature (\textdegree C) \\
& 4 & $k_p$ & Controller proportional gain \\
& 5 & $k_i$ & Controller integral gain \\
& 6 & $k_d$ & Controller derivative gain \\
& 7 & $v_{\max}$ & Voltage saturation (V) \\
& 8 & --- & Reserved \\
\midrule
\texttt{dynprm} & 0 & $t_e$ & Electrical time constant (s) \\
& 1 & $(di{/}dt)_{\max}$ & Current rate limit (A/s) \\
& 2 & $R_T$ & Thermal resistance ($\text{K}$/W) \\
& 3 & $C$ & Thermal capacitance (J/$\text{K}$) \\
& 4 & $T_a$ & Ambient temperature (\textdegree C) \\
& 5 & $\sigma_0$ & LuGre bristle stiffness \\
& 6 & $\sigma_1$ & LuGre bristle damping \\
& 7 & $s$ & Controller slew rate \\
& 8 & $I_{\max}$ & Integral limit (anti-windup) \\
\midrule
\texttt{biasprm} & 0 & $A$ & Cogging amplitude (N$\cdot$m) \\
& 1 & $N_p$ & Cogging periodicity \\
& 2 & $\phi$ & Cogging phase (rad) \\
& 3 & $\tau_c$ & LuGre Coulomb fric. (N$\cdot$m) \\
& 4 & $\tau_s$ & LuGre static fric. (N$\cdot$m) \\
& 5 & $\omega_s$ & Stribeck velocity (rad/s) \\
\midrule
\texttt{forcerange} & 0 & $-\tau_{\max}$ & Minimum torque (N$\cdot$m) \\
& 1 & $\tau_{\max}$ & Maximum torque (N$\cdot$m) \\
\midrule
\texttt{damping} & 0 & $B_1 (+\sigma_2)$ & Linear (+ LuGre viscous) \\
& 1 & $B_2$ & Quadratic \\
& 2 & $B_3$ & Cubic \\
\midrule
\texttt{armature} & 0 & $J_r$ & Actuator armature \\
\midrule
\texttt{gear} & 0 & $N$ & Gear ratio \\
\midrule
\texttt{ctrlspec} & 0 & --- & Input signature (\texttt{mjtCtrlInput} bitmask) \\
\bottomrule
\end{tabular}
\caption{\texttt{mjModel} array semantics for the \texttt{dcmotor} actuator.}
\label{tab:array_semantics}
\end{table}
\paragraph{Runtime mutability.}
Most \texttt{mjModel} parameters listed above may be freely modified at runtime for system identification or gain tuning. However, five parameters control the \emph{number} of activation states, which is determined at compile time and cannot change during simulation. Toggling any of the following parameters between zero and positive after compilation is an error:
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}llcl@{}}
\toprule
Parameter & Storage & State & Semantics \\
\midrule
$s$ & \texttt{dynprm[7]} & $u_{\text{prev}}$ & previous control \\
$k_i$ & \texttt{gainprm[5]} & $x_I$ & controller integral \\
$R_T, C$ & \texttt{dynprm[2,3]} & $T$ & temperature rise \\
$\sigma_0$ & \texttt{dynprm[5]} & $z$ & bristle deflection \\
$t_e$ & \texttt{dynprm[0]} & $i$ & winding current \\
\bottomrule
\end{tabular}
\caption{Compile-time state switches, allocated in \texttt{act} in the order shown. Do not toggle between zero and positive at runtime.}
\end{table}
\onecolumn
%=============================================================================
% DATASHEET MAPPING
%=============================================================================
\section{Datasheet Mapping}
\label{sec:datasheet}
Table~\ref{tab:datasheet} maps commercial motor datasheet specifications to attributes. The left column shows the datasheet entry as typically labeled by motor manufacturers; the right column shows the corresponding \texttt{dcmotor} MJCF attribute, when one exists. Derived quantities that are not direct attributes (gradient, mechanical time constant) are included for completeness.
\begin{table}[H]
\centering
\small
\begin{tabular}{@{}lllll@{}}
\toprule
Specification & Symbol & Formula / Note & Datasheet Symbol & Attribute \\
\midrule
Resistance & $R$ & Terminal resistance & R, Ra & \atR{} \\
Torque Constant & $K_t$ & $\tau = K_t i$ & kt, km & \atKt{} \\
Back-EMF Constant & $K_e$ & $v_{\text{back}} = K_e \omega$ & ke & \atKe{} \\
Speed Constant & $K_v$ & $K_v = 1/K_e$ & kn, kv & $1/$\atKe{} \\
Nominal Voltage & $v_n$ & Rated voltage (e.g., 24V) & Un, VDC & \atVM{} \\
No-load Speed & $\omega_0$ & $\omega_0 \approx v_n / K$ & n0 & \atNLS{} \\
Stall Torque & $\tau_0$ & $\tau_0 = K v_n / R$ & MH, Ts & \atSTALL{} \\
Stall Current & $i_s$ & Max.\ possible: $i_s = v_n / R$ & IA & \\
Nominal Current & $i_{\max}$ & Thermal limit (continuous) & Ic, IN & \atIMAX{} \\
Peak Current & $i_{\text{peak}}$ & Short-term limit & Ipk & \\
\midrule
Coulomb Friction & $\tau_c$ & Dry friction opposing motion & $T_f$ & \texttt{frictionloss} (joint)\\
Viscous Friction & $B$ & Drag $\propto \omega$ & $C_v$ & \texttt{damping} \\
Rotor Inertia & $J_r$ & Reflected: $J_{\text{eff}} = J_r N^2$ & J, Jm & \texttt{armature} \\
Gear Ratio & $N$ & Reduction ratio & $N$, $i$ & \texttt{gear} \\
Gearbox Efficiency & $\eta$ & Fold into $K$: use $\eta K$ & $\eta$ & \\
Cogging Amplitude & $A$ & Peak cogging torque & --- & \atCOGA{} \\
Cogging Periodicity & $N_p$ & Poles $\times$ slots/pole & --- & \atCOGP{} \\
\midrule
Nominal Torque & $\tau_n$ & $\tau_n = K \cdot i_{\max}$ & $M_N$, $T_c$ & \\
No-load Current & $i_0$ & Friction: Eq.~\eqref{eq:noload} & $I_0$ & \\
Gradient & $\partial\omega/\partial\tau$ & $-R / K^2$ & $\Delta n / \Delta M$ & \\
Mech.\ Time Const. & $t_m$ & $t_m = R\,J / K^2$ & $\tau_m$ & \\
\midrule
Inductance & $L$ & Terminal inductance & L & \atL{} \\
Elec.\ Time Const. & $t_e$ & $t_e = L / R$ & $\tau_e$ & \atTE{} \\
\midrule
Thermal Resistance & $R_T$ & Winding-to-ambient & Rth & \atRT{} \\
Thermal Capacitance & $C$ & $C = t_T / R_T$ & $C_{\text{th}}$ & \atTC{} \\
Thermal Time Const. & $t_T$ & $t_T = R_T C$ & $\tau_{\text{th}}$ & \atTT{} \\
Ref.\ Temperature & $T_0$ & Temperature at which $R$ is specified & $T_{\text{ref}}$ & \atTREF{} \\
Ambient Temperature & $T_a$ & Operating environment & --- & \atTAMB{} \\
Max.\ Winding Temp. & $T_{\max}$ & Absolute limit & $T_{\max}$ & \\
Res.\ Temp.\ Coeff. & $\alpha$ & $\approx 0.0039\, \text{K}^{-1}$ (copper) & $\alpha_{\text{Cu}}$ & \atALPHA{} \\
\bottomrule
\end{tabular}
\caption{Datasheet parameters and their relation to model constants. Groups: electrical, mechanical, derived, inductance, thermal.}
\label{tab:datasheet}
\end{table}
\small
\bibliographystyle{ieeetr}
\bibliography{refs}
\end{document}