diff --git a/src/engine/engine_support.c b/src/engine/engine_support.c
index b95aad8a..a58934f2 100644
--- a/src/engine/engine_support.c
+++ b/src/engine/engine_support.c
@@ -735,7 +735,7 @@ mjtNum mj_nextActivation(const mjModel* m, const mjData* d,
mjtNum F_S = biasprm[4]; // static friction
mjtNum v_S = biasprm[5]; // Stribeck velocity
mjtNum sigma0 = dynprm[5]; // bristle stiffness
- mjtNum velocity = d->actuator_velocity[actuator_id];
+ mjtNum velocity = d->actuator_velocity[m->actuator_outadr[actuator_id]];
mjtNum g = mj_lugreStribeck(velocity, F_C, F_S, v_S);
// ZOH exact ZOH integration: z(h) = exp(ah)*z(0) + ((exp(ah)-1)/a)*v
diff --git a/test/engine/engine_forward_test.cc b/test/engine/engine_forward_test.cc
index 43b790b8..a44e8e2b 100644
--- a/test/engine/engine_forward_test.cc
+++ b/test/engine/engine_forward_test.cc
@@ -1726,6 +1726,76 @@ TEST_F(DCMotorTest, LuGreViscousFriction) {
MjTol(1e-12, 1e-5));
}
+// the LuGre bristle must integrate the velocity of its own transmission:
+// placing a multi-output (SO3) actuator before the DC motor, so that the
+// motor's actuator id and output address diverge, must not change the
+// bristle dynamics
+TEST_F(DCMotorTest, LuGreBristleVelocityOrderInvariance) {
+ static constexpr char xml_dc_first[] = R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )";
+ static constexpr char xml_so3_first[] = R"(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )";
+ char error[1024];
+
+ // reference: DC motor first, actuator id == output address
+ MjModelPtr model = LoadModelFromString(xml_dc_first, error, sizeof(error));
+ ASSERT_THAT(model.get(), NotNull()) << error;
+ MjDataPtr data = MakeData(model);
+ int dc = mj_name2id(model.get(), mjOBJ_ACTUATOR, "dc");
+ data->qvel[model->jnt_dofadr[mj_name2id(model.get(), mjOBJ_JOINT, "hinge")]] = 1;
+ mj_step(model.get(), data.get());
+ double z_dc_first = data->act[model->actuator_actadr[dc]];
+
+ // reordered: the SO3 actuator has 3 outputs, so the DC motor now has
+ // actuator id 1 but output address 3
+ model = LoadModelFromString(xml_so3_first, error, sizeof(error));
+ ASSERT_THAT(model.get(), NotNull()) << error;
+ data = MakeData(model);
+ dc = mj_name2id(model.get(), mjOBJ_ACTUATOR, "dc");
+ ASSERT_EQ(model->actuator_outadr[dc], 3);
+ data->qvel[model->jnt_dofadr[mj_name2id(model.get(), mjOBJ_JOINT, "hinge")]] = 1;
+ mj_step(model.get(), data.get());
+ double z_so3_first = data->act[model->actuator_actadr[dc]];
+
+ // the bristle state saw the same spinning hinge in both models
+ EXPECT_NE(z_dc_first, 0);
+ EXPECT_MJTNUM_EQ(z_so3_first, z_dc_first);
+}
+
TEST_F(DCMotorTest, ThermalRiseAndFall) {
static constexpr char xml[] = R"(