Skip to content

Delayed multi-input actuators use uninitialized control slots #3597

Closed

Description

@peachtree0222

Intro

Hi!

I am testing the interaction between current multi-input actuators and actuator history/delay. The control-history path remains scalar and leaves the remaining inputs of a delayed actuator uninitialized.

My setup

  • MuJoCo source build from main at 10124d5d9dca411ec3c8988aa1e3b619103d71bb
  • C API, double precision
  • Ubuntu 24.04.5 LTS, Linux 7.0.0-28-generic x86_64
  • CMake 3.28.3 and GCC 11.5.0

What's happening? What did you expect?

A PID actuator with input="pos vel ff" owns three control values. With live controls [1, 2, 3], its force is 6. I expected the delayed path to record and later use the same complete control block.

Actuator history allocation, insertion, mj_readCtrl, and mj_initCtrlHistory are hard-coded to dimension one. During actuation, a local array of all nu controls is allocated, but a delayed actuator assigns only ctrl[adr]. The remaining two slots are consumed by the PID gain without initialization.

The minimal reproduction reuses MuJoCo's stack allocation with canaries so the omitted slots are deterministic:

nu=3 delayed-force=501.0 direct-force=6.0

The delayed path uses the recorded first input 1 plus stale stack values 200 and 300. Disabling only delay uses the live complete block and produces 6.

Relevant source and documentation:

  • mujoco/src/user/user_model.cc

    Lines 2213 to 2224 in 10124d5

    // nsensordata
    for (int i = 0; i < nsensor; i++) { nsensordata += sensors_[i]->dim; }
    // nhistory: layout is [user, cursor, times(n), values(n*dim)] = 2+2n per actuator (dim=1)
    nhistory = 0;
    for (int i = 0; i < actuators_.size(); i++) {
    if (actuators_[i]->nsample > 0) { nhistory += 2 + 2 * actuators_[i]->nsample; }
    }
    // sensor delay: layout is [user, cursor, times(n), values(n*dim)] = 2 + n + n*dim
    for (int i = 0; i < sensors_.size(); i++) {
    if (sensors_[i]->nsample > 0) {
    nhistory += 2 + sensors_[i]->nsample + sensors_[i]->nsample * sensors_[i]->dim;
  • mujoco/src/user/user_model.cc

    Lines 3863 to 3872 in 10124d5

    // historyadr
    m->actuator_delay[i] = (mjtNum)pac->delay;
    m->actuator_history[2 * i] = pac->nsample;
    m->actuator_history[2 * i + 1] = pac->interp;
    if (pac->nsample > 0) {
    m->actuator_historyadr[i] = delay_adr;
    delay_adr += 2 + 2 * pac->nsample; // [user, cursor, times, values]
    } else {
    m->actuator_historyadr[i] = -1;
    }
  • // local copy of ctrl
    mj_markStack(d);
    mjtNum *ctrl = mjSTACKALLOC(d, nu, mjtNum);
    // read from ctrl or history buffer for delayed actuators
    for (int i = 0; i < nactuator; i++) {
    int adr = m->actuator_ctrladr[i];
    if (m->actuator_delay[i]) {
    // delayed: read from history buffer (scalar input)
    int interp = m->actuator_history[2*i+1];
    ctrl[adr] = mj_readCtrl(m, d, i, d->time, interp);
    } else {
    mju_copy(ctrl + adr, d->ctrl + adr, m->actuator_ctrlnum[i]);
    }
    }
  • // the work every integrator does at the pre-step state before it writes the new one: history
    // buffers, activations, sleep
    // act_dot: activation derivatives
    static void advanceStart(const mjModel* m, mjData* d, const mjtNum* act_dot) {
    int nactuator = m->nactuator, nsensor = m->nsensor;
    // advance history buffers
    if (m->nhistory > 0) {
    // advance ctrl history buffers
    for (int i = 0; i < nactuator; i++) {
    int nsample = m->actuator_history[2*i];
    if (nsample == 0) continue;
    // get history buffer pointer and insert ctrl at current time
    mjtNum* buf = d->history + m->actuator_historyadr[i];
    *mju_historyInsert(buf, nsample, /*dim=*/1, d->time) = d->ctrl[m->actuator_ctrladr[i]];
    }
  • // read delayed ctrl value for actuator at given time
    mjtNum mj_readCtrl(const mjModel* m, const mjData* d, int id, mjtNum time, int interp) {
    // validate actuator id
    if (id < 0 || id >= m->nactuator) {
    mjERROR("invalid actuator id %d", id);
    return 0;
    }
    // no delay: return current ctrl value
    int nsample = m->actuator_history[2*id];
    if (nsample == 0) {
    return d->ctrl[m->actuator_ctrladr[id]];
    }
    // resolve interpolation order: use model's interp if argument is -1
    if (interp < 0) interp = m->actuator_history[2*id+1];
    // get buffer pointer and read from history buffer
    mjtNum delay = m->actuator_delay[id];
    const mjtNum* buf = d->history + m->actuator_historyadr[id];
    mjtNum res;
    const mjtNum* ptr = mju_historyRead(buf, nsample, /*dim=*/1, &res, time - delay, interp);
    return ptr ? *ptr : res;
    }
    // read sensor value from history buffer at given time
    const mjtNum* mj_readSensor(const mjModel* m, const mjData* d, int id, mjtNum time,
    mjtNum* result, int interp) {
    // validate sensor id
    if (id < 0 || id >= m->nsensor) {
    mjERROR("invalid sensor id %d", id);
    return NULL;
    }
    // no history: return current sensor value
    int nsample = m->sensor_history[2*id];
    if (nsample == 0) {
    return d->sensordata + m->sensor_adr[id];
    }
    // resolve interpolation order: use model's interp if argument is -1
    if (interp < 0) interp = m->sensor_history[2*id+1];
    // get buffer pointer and read from history buffer
    int dim = m->sensor_dim[id];
    mjtNum delay = m->sensor_delay[id];
    const mjtNum* buf = d->history + m->sensor_historyadr[id];
    return mju_historyRead(buf, nsample, dim, result, time - delay, interp);
    }
    // initialize history buffer for actuator
    void mj_initCtrlHistory(const mjModel* m, mjData* d, int id,
    const mjtNum* times, const mjtNum* values) {
    // validate actuator id
    if (id < 0 || id >= m->nactuator) {
    mjERROR("invalid actuator id %d", id);
    return;
    }
    // check that actuator has a history buffer
    int nsample = m->actuator_history[2*id];
    if (nsample == 0) {
    mjERROR("actuator %d has no history buffer", id);
    return;
    }
    // get buffer pointer
    mjtNum* buf = d->history + m->actuator_historyadr[id];
    // if times is NULL, use existing buffer times
    const mjtNum* buf_times = times ? times : buf + 2;
    // get existing user value (preserve it)
    mjtNum user = buf[0];
    // initialize history buffer
    mju_historyInit(buf, nsample, 1, buf_times, values, user);
    }

Steps for reproduction

  1. Check out and build the commit above.
  2. Compile poc/reproducer.cc using poc/README.md.
  3. Run it and compare delayed force with the direct-control causal control.

Reproduction package:

multi-input-actuator-delay-history-poc.zip

Minimal model for reproduction

The complete MJCF is embedded in the C++ file and contains one slider, one sphere, and one delayed three-input PID. No assets are required.

Code required for reproduction

See poc/reproducer.cc. A complete patch needs a block-aware history layout and API decision, so I have included a fix design rather than a partial one-line change.

Confirmations

Activity

  1. added a commit that references this issue on Sep 20, 2026
    dc8bb13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      ⌂ 换网址