Skip to content

Commit b0fa41f

Browse files
committed
feat: implement Articulated Body Algorithm (ABA) for forward dynamics (1.6μs, faster than Pinocchio)
1 parent d30c1ac commit b0fa41f

5 files changed

Lines changed: 153 additions & 6 deletions

File tree

docs/src/lib/dynamics.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Pages = ["ModernRoboticsBook.jl"]
1111
Filter = t -> nameof(t) in [
1212
:ad, :inverse_dynamics_rnea, :inverse_dynamics_rnea!, :mass_matrix_crba, :mass_matrix_crba!,
1313
:mass_matrix_rnea, :velocity_quadratic_forces, :gravity_forces,
14-
:end_effector_forces, :forward_dynamics_crba, :forward_dynamics_rnea,
14+
:end_effector_forces, :forward_dynamics_aba, :forward_dynamics_crba, :forward_dynamics_rnea,
1515
:euler_step, :inverse_dynamics_trajectory, :forward_dynamics_trajectory,
1616
]
1717
```

docs/src/man/benchmarks.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ This page compares the performance of ModernRoboticsBook.jl against [Pinocchio](
1212
| Mass matrix (CRBA) | 1.14 μs | **0.91 μs** | 0.65 μs | 0.14 μs |
1313
| Mass matrix (RNEA) | 6.79 μs ||||
1414
| Gravity / dynamics bias | 1.19 μs || 0.48 μs | 0.55 μs |
15-
| Forward dynamics (CRBA + RNEA) | 2.71 μs || 2.82 μs ||
15+
| Forward dynamics (ABA) | 1.60 μs || 2.82 μs ||
16+
| Forward dynamics (CRBA + RNEA) | 2.80 μs ||||
1617
| Forward dynamics (RNEA) | 5.44 μs ||||
1718

1819
*Measured on Apple M2 (16 GB), Julia 1.12, Python 3.13. Julia timings are median values from BenchmarkTools.jl. RBD.jl: the ee\_link frame is grabbed before removing fixed joints; timings are for in-place variants where available.*
@@ -31,7 +32,7 @@ The textbook algorithm variants (`mass_matrix_rnea`, `forward_dynamics_rnea`) ar
3132

3233
- **Inverse dynamics**: Uses the **Recursive Newton-Euler Algorithm (RNEA)**, the same O(n) algorithm as Pinocchio and RBD.jl. The in-place variant achieves zero allocations.
3334
- **Mass matrix**: Uses the **Composite Rigid Body Algorithm (CRBA)**, which computes the full matrix in a single backward pass over composite spatial inertias — the same algorithm as Pinocchio and RBD.jl. The in-place variant achieves zero allocations. The textbook variant `mass_matrix_rnea` calls `inverse_dynamics_rnea` n times with unit accelerations.
34-
- **Forward dynamics**: Computes `M \ (τ - RNEA(q, dq, 0, g, F))` using CRBA for the mass matrix and a single RNEA call for the bias forces. Pinocchio uses the **Articulated Body Algorithm (ABA)**, which solves for joint accelerations in O(n) without forming the mass matrix. The textbook variant `forward_dynamics_rnea` explicitly forms M⁻¹ and calls RNEA separately for each term.
35+
- **Forward dynamics**: Three implementations are available: `forward_dynamics_aba` uses the **Articulated Body Algorithm** — the same O(n) algorithm as Pinocchio — which solves for joint accelerations without forming the mass matrix. `forward_dynamics_crba` uses CRBA + a single RNEA call + backslash solve. `forward_dynamics_rnea` is the textbook variant that explicitly forms M⁻¹ and calls RNEA separately for each term.
3536

3637
### Allocations
3738

@@ -41,6 +42,7 @@ The textbook algorithm variants (`mass_matrix_rnea`, `forward_dynamics_rnea`) ar
4142
| Jacobian | 2 | 384 B | **0 (0 B)** |
4243
| Inverse dynamics (RNEA) | 13 | 3.4 KiB | **0 (0 B)** |
4344
| Mass matrix (CRBA) | 9 | 4.6 KiB | **0 (0 B)** |
45+
| Forward dynamics (ABA) | 23 | 6.1 KiB ||
4446
| Forward dynamics | 32 | 8.8 KiB ||
4547

4648
### When does this matter?

src/ModernRoboticsBook.jl

Lines changed: 121 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export near_zero,
4444
velocity_quadratic_forces,
4545
gravity_forces,
4646
end_effector_forces,
47+
forward_dynamics_aba,
4748
forward_dynamics_crba,
4849
forward_dynamics_rnea,
4950
euler_step,
@@ -1804,9 +1805,17 @@ Computes forward dynamics in the space frame for an open chain robot.
18041805
!!! details "Educational note"
18051806
The textbook computes forward dynamics by explicitly forming ``M^{-1}`` and calling
18061807
[`inverse_dynamics_rnea`](@ref) separately for Coriolis, gravity, and tip wrench terms.
1807-
This implementation combines these into a single RNEA call and uses `M \\ b` (LU
1808-
factorization) instead of explicit inversion, which is both faster and more
1809-
numerically stable. See [`forward_dynamics_rnea`](@ref) for the textbook algorithm.
1808+
This implementation makes two improvements:
1809+
1810+
1. **Single RNEA call**: instead of computing ``c``, ``g``, and ``J^T F`` separately,
1811+
a single call to RNEA with zero accelerations returns ``\\tau_{\\text{bias}} = c + g + J^T F``.
1812+
2. **Backslash solve** (`M \\ b`): instead of explicitly forming ``M^{-1}`` and
1813+
multiplying, Julia's `\\` operator solves the linear system ``M x = b`` via LU
1814+
factorization. This is both faster (avoids forming the inverse) and more
1815+
numerically stable (fewer floating-point operations, better conditioning).
1816+
1817+
For the fastest forward dynamics, use [`forward_dynamics_aba`](@ref) which avoids
1818+
forming ``M`` entirely. See [`forward_dynamics_rnea`](@ref) for the textbook algorithm.
18101819
18111820
# Arguments
18121821
- `joint_positions`: the ``n``-vector of joint variables.
@@ -1856,6 +1865,115 @@ function forward_dynamics_crba(
18561865
return mass \ tau_rhs
18571866
end
18581867

1868+
"""
1869+
forward_dynamics_aba(joint_positions, joint_velocities, joint_torques, gravity, tip_wrench, link_frames, spatial_inertias, screw_axes)
1870+
1871+
Computes forward dynamics using the **Articulated Body Algorithm (ABA)**.
1872+
1873+
!!! info "Articulated Body Algorithm"
1874+
ABA solves for joint accelerations in O(n) with three passes over the kinematic
1875+
chain, without forming or inverting the mass matrix:
1876+
1877+
1. **Outward pass**: compute link velocities and velocity-dependent bias forces.
1878+
2. **Inward pass**: accumulate *articulated body inertias* — the effective inertia
1879+
of each subtree accounting for joint freedom — from leaf to root.
1880+
3. **Outward pass**: solve for joint accelerations from root to leaf.
1881+
1882+
This is the same algorithm Pinocchio uses for forward dynamics.
1883+
1884+
!!! details "Educational note"
1885+
The textbook (Chapter 8.3) computes forward dynamics by explicitly forming
1886+
``M(\\theta)`` and solving ``M \\ddot{\\theta} = \\tau - c - g``. See
1887+
[`forward_dynamics_crba`](@ref) for that approach. ABA avoids forming ``M``
1888+
entirely, which is asymptotically faster for large ``n``.
1889+
1890+
# Arguments
1891+
- `joint_positions`: the ``n``-vector of joint variables.
1892+
- `joint_velocities`: the ``n``-vector of joint rates.
1893+
- `joint_torques`: the ``n``-vector of joint forces/torques.
1894+
- `gravity`: the gravity vector ``g`` (e.g., `[0, 0, -9.8]`).
1895+
- `tip_wrench`: the wrench ``\\mathcal{F}_{\\text{tip}}`` applied by the end-effector expressed in frame ``\\{n+1\\}``.
1896+
- `link_frames`: a vector of ``n+1`` SE(3) matrices, where `link_frames[i]` is ``M_{i-1,i}`` and `link_frames[n+1]` is ``M_{n,n+1}``.
1897+
- `spatial_inertias`: a vector of ``n`` symmetric 6×6 spatial inertia matrices ``G_i`` of the links.
1898+
- `screw_axes`: the screw axes ``S_i`` of the joints in a space frame, as a 6×``n`` matrix with axes as columns.
1899+
1900+
# Returns
1901+
The ``n``-vector of joint accelerations ``\\ddot{\\theta}``.
1902+
"""
1903+
function forward_dynamics_aba(
1904+
joint_positions::AbstractVector,
1905+
joint_velocities::AbstractVector,
1906+
joint_torques::AbstractVector,
1907+
gravity::AbstractVector,
1908+
tip_wrench::AbstractVector,
1909+
link_frames::AbstractVector,
1910+
spatial_inertias::AbstractVector,
1911+
screw_axes::AbstractMatrix,
1912+
)
1913+
n = length(joint_positions)
1914+
1915+
# Pass 1 (outward): velocities, bias accelerations, bias forces
1916+
Mi = SMatrix{4,4,Float64}(LA.I)
1917+
Ai = Vector{SVector{6,Float64}}(undef, n)
1918+
AdTi = Vector{SMatrix{6,6,Float64,36}}(undef, n + 1)
1919+
Vi = Vector{SVector{6,Float64}}(undef, n + 1)
1920+
ci = Vector{SVector{6,Float64}}(undef, n)
1921+
IA = Vector{SMatrix{6,6,Float64,36}}(undef, n)
1922+
pA = Vector{SVector{6,Float64}}(undef, n)
1923+
1924+
Vi[1] = @SVector zeros(6)
1925+
AdTi[n+1] = adjoint_representation(transform_inv(link_frames[n+1]))
1926+
1927+
for i = 1:n
1928+
Mi = Mi * SMatrix{4,4}(link_frames[i])
1929+
Ai[i] =
1930+
adjoint_representation(transform_inv(Mi)) * SVector{6}(@view screw_axes[:, i])
1931+
AdTi[i] = adjoint_representation(
1932+
matrix_exp6(vec_to_se3(Ai[i] * -joint_positions[i])) *
1933+
transform_inv(SMatrix{4,4}(link_frames[i])),
1934+
)
1935+
Vi[i+1] = AdTi[i] * Vi[i] + Ai[i] * joint_velocities[i]
1936+
ci[i] = ad(Vi[i+1]) * Ai[i] * joint_velocities[i]
1937+
Gi = SMatrix{6,6}(spatial_inertias[i])
1938+
IA[i] = Gi
1939+
pA[i] = -ad(Vi[i+1])' * Gi * Vi[i+1]
1940+
end
1941+
1942+
pA[n] = pA[n] + AdTi[n+1]' * SVector{6}(tip_wrench)
1943+
1944+
# Pass 2 (inward): articulated body inertias and bias forces
1945+
U = Vector{SVector{6,Float64}}(undef, n)
1946+
D = Vector{Float64}(undef, n)
1947+
u = Vector{Float64}(undef, n)
1948+
1949+
for i = n:-1:1
1950+
U[i] = IA[i] * Ai[i]
1951+
D[i] = Ai[i]' * U[i]
1952+
u[i] = joint_torques[i] - Ai[i]' * pA[i]
1953+
if i > 1
1954+
Ia = IA[i] - U[i] * U[i]' / D[i]
1955+
pa = pA[i] + Ia * ci[i] + U[i] * u[i] / D[i]
1956+
IA[i-1] = IA[i-1] + AdTi[i]' * Ia * AdTi[i]
1957+
pA[i-1] = pA[i-1] + AdTi[i]' * pa
1958+
end
1959+
end
1960+
1961+
# Pass 3 (outward): joint accelerations
1962+
ddq = zeros(n)
1963+
a0 = SA[0.0, 0.0, 0.0, -gravity[1], -gravity[2], -gravity[3]]
1964+
a = AdTi[1] * a0
1965+
1966+
for i = 1:n
1967+
a_plus_c = a + ci[i]
1968+
ddq[i] = (u[i] - U[i]' * a_plus_c) / D[i]
1969+
if i < n
1970+
a = AdTi[i+1] * (a_plus_c + Ai[i] * ddq[i])
1971+
end
1972+
end
1973+
1974+
return ddq
1975+
end
1976+
18591977
"""
18601978
forward_dynamics_rnea(joint_positions, joint_velocities, joint_torques, gravity, tip_wrench, link_frames, spatial_inertias, screw_axes)
18611979

src/robot.jl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,26 @@ function forward_dynamics_crba(
268268
)
269269
end
270270

271+
function forward_dynamics_aba(
272+
robot::Robot,
273+
joint_positions::AbstractVector,
274+
joint_velocities::AbstractVector,
275+
joint_torques::AbstractVector;
276+
gravity::AbstractVector = robot.gravity,
277+
tip_wrench::AbstractVector = zeros(6),
278+
)
279+
forward_dynamics_aba(
280+
joint_positions,
281+
joint_velocities,
282+
joint_torques,
283+
gravity,
284+
tip_wrench,
285+
robot.link_frames,
286+
robot.spatial_inertias,
287+
robot.screw_axes_space,
288+
)
289+
end
290+
271291
# Convenience wrappers: trajectory dynamics
272292

273293
function inverse_dynamics_trajectory(

test/runtests.jl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,6 +1156,13 @@ Aqua.test_all(ModernRoboticsBook)
11561156
tau_ref;
11571157
tip_wrench = zeros(6),
11581158
) ddq_ref atol = 1e-6
1159+
@test forward_dynamics_aba(
1160+
robot,
1161+
q,
1162+
v,
1163+
tau_ref;
1164+
tip_wrench = zeros(6),
1165+
) ddq_ref atol = 1e-6
11591166

11601167
# Jacobians: Pinocchio uses [v; ω] convention, we use [ω; v],
11611168
# so swap the top/bottom 3-row blocks before comparing.

0 commit comments

Comments
 (0)