Repository navigation
Description
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
mainat10124d5d9dca411ec3c8988aa1e3b619103d71bb - 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:
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; 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; } mujoco/src/engine/engine_forward.c
Lines 386 to 400 in 10124d5
// 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]); } } mujoco/src/engine/engine_forward.c
Lines 1344 to 1360 in 10124d5
// 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]]; } mujoco/src/engine/engine_support.c
Lines 934 to 1013 in 10124d5
// 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
- Check out and build the commit above.
- Compile
poc/reproducer.ccusingpoc/README.md. - 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
- I searched the latest documentation thoroughly before posting.
- I searched previous Issues and Discussions, I am certain this has not been raised before.
Activity
Support multi-input (MIMO) actuators in control history buffers and m…