rollout accepts a list of models of length nroll

This commit is contained in:
Levi Burner
2024-11-24 16:50:28 -05:00
parent 6e129a5ab2
commit 943eb6bc7e
5 changed files with 102 additions and 43 deletions
+1
View File
@@ -16,6 +16,7 @@ Python bindings
- Added ``bind`` method and removed id attribute from :ref:`mjSpec` objects. Using ids is error prone in scenarios of repeated attachment and - Added ``bind`` method and removed id attribute from :ref:`mjSpec` objects. Using ids is error prone in scenarios of repeated attachment and
detachment. Python users are encouraged to use names for unique identification of model elements. detachment. Python users are encouraged to use names for unique identification of model elements.
- Removed ``nroll`` argument from :ref:`rollout<PyRollout>` because its value can always be inferred. - Removed ``nroll`` argument from :ref:`rollout<PyRollout>` because its value can always be inferred.
- :ref:`rollout<PyRollout>` can now accept lists of MjModel of length ``nroll``.
Bug fixes Bug fixes
^^^^^^^^^ ^^^^^^^^^
+1
View File
@@ -700,6 +700,7 @@ states and sensor values. The basic usage form is
state, sensordata = rollout.rollout(model, data, initial_state, control) state, sensordata = rollout.rollout(model, data, initial_state, control)
``model`` is either a single instance of MjModel or a list of compatible MjModel of length ``nroll``.
``initial_state`` is an ``nroll x nstate`` array, with ``nroll`` initial states of size ``nstate``, where ``initial_state`` is an ``nroll x nstate`` array, with ``nroll`` initial states of size ``nstate``, where
``nstate = mj_stateSize(model, mjtState.mjSTATE_FULLPHYSICS)`` is the size of the ``nstate = mj_stateSize(model, mjtState.mjSTATE_FULLPHYSICS)`` is the size of the
:ref:`full physics state<geFullPhysics>`. ``control`` is a ``nroll x nstep x ncontrol`` array of controls. Controls are :ref:`full physics state<geFullPhysics>`. ``control`` is a ``nroll x nstep x ncontrol`` array of controls. Controls are
+29 -26
View File
@@ -37,7 +37,7 @@ const auto rollout_doc = R"(
Roll out open-loop trajectories from initial states, get resulting states and sensor values. Roll out open-loop trajectories from initial states, get resulting states and sensor values.
input arguments (required): input arguments (required):
model instance of MjModel model list of MjModel instances of length nroll
data associated instance of MjData data associated instance of MjData
nstep integer, number of steps to be taken for each trajectory nstep integer, number of steps to be taken for each trajectory
control_spec specification of controls, ncontrol = mj_stateSize(m, control_spec) control_spec specification of controls, ncontrol = mj_stateSize(m, control_spec)
@@ -54,18 +54,18 @@ Roll out open-loop trajectories from initial states, get resulting states and se
// C-style rollout function, assumes all arguments are valid // C-style rollout function, assumes all arguments are valid
// all input fields of d are initialised, contents at call time do not matter // all input fields of d are initialised, contents at call time do not matter
// after returning, d will contain the last step of the last rollout // after returning, d will contain the last step of the last rollout
void _unsafe_rollout(const mjModel* m, mjData* d, int nroll, int nstep, unsigned int control_spec, void _unsafe_rollout(const mjModel** m, mjData* d, int nroll, int nstep, unsigned int control_spec,
const mjtNum* state0, const mjtNum* warmstart0, const mjtNum* control, const mjtNum* state0, const mjtNum* warmstart0, const mjtNum* control,
mjtNum* state, mjtNum* sensordata) { mjtNum* state, mjtNum* sensordata) {
// sizes // sizes
int nstate = mj_stateSize(m, mjSTATE_FULLPHYSICS); int nstate = mj_stateSize(m[0], mjSTATE_FULLPHYSICS);
int ncontrol = mj_stateSize(m, control_spec); int ncontrol = mj_stateSize(m[0], control_spec);
int nv = m->nv, nbody = m->nbody, neq = m->neq; int nv = m[0]->nv, nbody = m[0]->nbody, neq = m[0]->neq;
int nsensordata = m->nsensordata; int nsensordata = m[0]->nsensordata;
// clear user inputs if unspecified // clear user inputs if unspecified
if (!(control_spec & mjSTATE_CTRL)) { if (!(control_spec & mjSTATE_CTRL)) {
mju_zero(d->ctrl, m->nu); mju_zero(d->ctrl, m[0]->nu);
} }
if (!(control_spec & mjSTATE_QFRC_APPLIED)) { if (!(control_spec & mjSTATE_QFRC_APPLIED)) {
mju_zero(d->qfrc_applied, nv); mju_zero(d->qfrc_applied, nv);
@@ -75,26 +75,26 @@ void _unsafe_rollout(const mjModel* m, mjData* d, int nroll, int nstep, unsigned
} }
if (!(control_spec & mjSTATE_MOCAP_POS)) { if (!(control_spec & mjSTATE_MOCAP_POS)) {
for (int i = 0; i < nbody; i++) { for (int i = 0; i < nbody; i++) {
int id = m->body_mocapid[i]; int id = m[0]->body_mocapid[i];
if (id >= 0) mju_copy3(d->mocap_pos+3*id, m->body_pos+3*i); if (id >= 0) mju_copy3(d->mocap_pos+3*id, m[0]->body_pos+3*i);
} }
} }
if (!(control_spec & mjSTATE_MOCAP_QUAT)) { if (!(control_spec & mjSTATE_MOCAP_QUAT)) {
for (int i = 0; i < nbody; i++) { for (int i = 0; i < nbody; i++) {
int id = m->body_mocapid[i]; int id = m[0]->body_mocapid[i];
if (id >= 0) mju_copy4(d->mocap_quat+4*id, m->body_quat+4*i); if (id >= 0) mju_copy4(d->mocap_quat+4*id, m[0]->body_quat+4*i);
} }
} }
if (!(control_spec & mjSTATE_EQ_ACTIVE)) { if (!(control_spec & mjSTATE_EQ_ACTIVE)) {
for (int i = 0; i < neq; i++) { for (int i = 0; i < neq; i++) {
d->eq_active[i] = m->eq_active0[i]; d->eq_active[i] = m[0]->eq_active0[i];
} }
} }
// loop over rollouts // loop over rollouts
for (int r = 0; r < nroll; r++) { for (int r = 0; r < nroll; r++) {
// set initial state // set initial state
mj_setState(m, d, state0 + r*nstate, mjSTATE_FULLPHYSICS); mj_setState(m[r], d, state0 + r*nstate, mjSTATE_FULLPHYSICS);
// set warmstart accelerations // set warmstart accelerations
if (warmstart0) { if (warmstart0) {
@@ -124,7 +124,7 @@ void _unsafe_rollout(const mjModel* m, mjData* d, int nroll, int nstep, unsigned
for (; t < nstep; t++) { for (; t < nstep; t++) {
int step = r*nstep + t; int step = r*nstep + t;
if (state) { if (state) {
mj_getState(m, d, state + step*nstate, mjSTATE_FULLPHYSICS); mj_getState(m[r], d, state + step*nstate, mjSTATE_FULLPHYSICS);
} }
if (sensordata) { if (sensordata) {
mju_copy(sensordata + step*nsensordata, d->sensordata, nsensordata); mju_copy(sensordata + step*nsensordata, d->sensordata, nsensordata);
@@ -137,15 +137,15 @@ void _unsafe_rollout(const mjModel* m, mjData* d, int nroll, int nstep, unsigned
// controls // controls
if (control) { if (control) {
mj_setState(m, d, control + step*ncontrol, control_spec); mj_setState(m[r], d, control + step*ncontrol, control_spec);
} }
// step // step
mj_step(m, d); mj_step(m[r], d);
// copy out new state // copy out new state
if (state) { if (state) {
mj_getState(m, d, state + step*nstate, mjSTATE_FULLPHYSICS); mj_getState(m[r], d, state + step*nstate, mjSTATE_FULLPHYSICS);
} }
// copy out sensor values // copy out sensor values
@@ -188,7 +188,7 @@ PYBIND11_MODULE(_rollout, pymodule) {
// get subsequent states and corresponding sensor values // get subsequent states and corresponding sensor values
pymodule.def( pymodule.def(
"rollout", "rollout",
[](const MjModelWrapper& m, MjDataWrapper& d, [](py::list m, MjDataWrapper& d,
int nstep, unsigned int control_spec, int nstep, unsigned int control_spec,
const PyCArray state0, const PyCArray state0,
std::optional<const PyCArray> warmstart0, std::optional<const PyCArray> warmstart0,
@@ -196,7 +196,12 @@ PYBIND11_MODULE(_rollout, pymodule) {
std::optional<const PyCArray> state, std::optional<const PyCArray> state,
std::optional<const PyCArray> sensordata std::optional<const PyCArray> sensordata
) { ) {
const raw::MjModel* model = m.get(); // get raw pointers
int nroll = state0.shape(0);
const raw::MjModel* model_ptrs[nroll];
for (int r = 0; r < nroll; r++) {
model_ptrs[r] = m[r].cast<const MjModelWrapper*>()->get();
}
raw::MjData* data = d.get(); raw::MjData* data = d.get();
// check that some steps need to be taken, return if not // check that some steps need to be taken, return if not
@@ -205,19 +210,17 @@ PYBIND11_MODULE(_rollout, pymodule) {
} }
// get sizes // get sizes
int nstate = mj_stateSize(model, mjSTATE_FULLPHYSICS); int nstate = mj_stateSize(model_ptrs[0], mjSTATE_FULLPHYSICS);
int ncontrol = mj_stateSize(model, control_spec); int ncontrol = mj_stateSize(model_ptrs[0], control_spec);
int nroll = state0.shape(0);
// get raw pointers
mjtNum* state0_ptr = get_array_ptr(state0, "state0", nroll, 1, nstate); mjtNum* state0_ptr = get_array_ptr(state0, "state0", nroll, 1, nstate);
mjtNum* warmstart0_ptr = get_array_ptr(warmstart0, "warmstart0", nroll, mjtNum* warmstart0_ptr = get_array_ptr(warmstart0, "warmstart0", nroll,
1, model->nv); 1, model_ptrs[0]->nv);
mjtNum* control_ptr = get_array_ptr(control, "control", nroll, mjtNum* control_ptr = get_array_ptr(control, "control", nroll,
nstep, ncontrol); nstep, ncontrol);
mjtNum* state_ptr = get_array_ptr(state, "state", nroll, nstep, nstate); mjtNum* state_ptr = get_array_ptr(state, "state", nroll, nstep, nstate);
mjtNum* sensordata_ptr = get_array_ptr(sensordata, "sensordata", nroll, mjtNum* sensordata_ptr = get_array_ptr(sensordata, "sensordata", nroll,
nstep, model->nsensordata); nstep, model_ptrs[0]->nsensordata);
// perform rollouts // perform rollouts
{ {
@@ -226,7 +229,7 @@ PYBIND11_MODULE(_rollout, pymodule) {
// call unsafe rollout function // call unsafe rollout function
InterceptMjErrors(_unsafe_rollout)( InterceptMjErrors(_unsafe_rollout)(
model, data, nroll, nstep, control_spec, state0_ptr, model_ptrs, data, nroll, nstep, control_spec, state0_ptr,
warmstart0_ptr, control_ptr, state_ptr, sensordata_ptr); warmstart0_ptr, control_ptr, state_ptr, sensordata_ptr);
} }
}, },
+34 -13
View File
@@ -14,7 +14,7 @@
# ============================================================================== # ==============================================================================
"""Roll out open-loop trajectories from initial states, get subsequent states and sensor values.""" """Roll out open-loop trajectories from initial states, get subsequent states and sensor values."""
from typing import Optional from typing import Optional, Union
import mujoco import mujoco
from mujoco import _rollout from mujoco import _rollout
@@ -22,7 +22,7 @@ import numpy as np
from numpy import typing as npt from numpy import typing as npt
def rollout(model: mujoco.MjModel, def rollout(model: Union[mujoco.MjModel, list[mujoco.MjModel]],
data: mujoco.MjData, data: mujoco.MjData,
initial_state: npt.ArrayLike, initial_state: npt.ArrayLike,
control: Optional[npt.ArrayLike] = None, control: Optional[npt.ArrayLike] = None,
@@ -41,7 +41,7 @@ def rollout(model: mujoco.MjModel,
Allocates outputs if none are given. Allocates outputs if none are given.
Args: Args:
model: An mjModel instance. model: An mjModel or a list of MjModel with the same size signature.
data: An associated mjData instance. data: An associated mjData instance.
initial_state: Array of initial states from which to roll out trajectories. initial_state: Array of initial states from which to roll out trajectories.
([nroll or 1] x nstate) ([nroll or 1] x nstate)
@@ -90,6 +90,7 @@ def rollout(model: mujoco.MjModel,
state=state, state=state,
sensordata=sensordata) sensordata=sensordata)
# check number of dimensions # check number of dimensions
_check_number_of_dimensions(2, _check_number_of_dimensions(2,
initial_state=initial_state, initial_state=initial_state,
@@ -108,14 +109,6 @@ def rollout(model: mujoco.MjModel,
state = _ensure_3d(state) state = _ensure_3d(state)
sensordata = _ensure_3d(sensordata) sensordata = _ensure_3d(sensordata)
# check trailing dimensions
nstate = mujoco.mj_stateSize(model, mujoco.mjtState.mjSTATE_FULLPHYSICS.value)
_check_trailing_dimension(nstate, initial_state=initial_state, state=state)
ncontrol = mujoco.mj_stateSize(model, control_spec)
_check_trailing_dimension(ncontrol, control=control)
_check_trailing_dimension(model.nv, initial_warmstart=initial_warmstart)
_check_trailing_dimension(model.nsensordata, sensordata=sensordata)
# infer nroll, check for incompatibilities # infer nroll, check for incompatibilities
nroll = _infer_dimension(0, 1, nroll = _infer_dimension(0, 1,
initial_state=initial_state, initial_state=initial_state,
@@ -123,6 +116,14 @@ def rollout(model: mujoco.MjModel,
control=control, control=control,
state=state, state=state,
sensordata=sensordata) sensordata=sensordata)
if isinstance(model, list) and nroll == 1:
nroll = len(model)
if isinstance(model, list) and len(model) != nroll:
raise ValueError(f'nroll inferred as {nroll} '
f'but model is length {len(model)}')
elif not isinstance(model, list):
model = [model] # Use a length 1 list to simplify code below
# infer nstep, check for incompatibilities # infer nstep, check for incompatibilities
nstep = _infer_dimension(1, nstep or 1, nstep = _infer_dimension(1, nstep or 1,
@@ -130,7 +131,27 @@ def rollout(model: mujoco.MjModel,
state=state, state=state,
sensordata=sensordata) sensordata=sensordata)
# tile input arrays if required (singleton expansion) # get nstate/ncontrol/nv/nsensordata
# check that they are equal across models
nstate = mujoco.mj_stateSize(model[0], mujoco.mjtState.mjSTATE_FULLPHYSICS.value)
ncontrol = mujoco.mj_stateSize(model[0], control_spec)
nv = model[0].nv
nsensordata = model[0].nsensordata
for m in model[1:]:
if (nstate != mujoco.mj_stateSize(m, mujoco.mjtState.mjSTATE_FULLPHYSICS.value)
or ncontrol != mujoco.mj_stateSize(m, control_spec)
or nv != m.nv
or nsensordata != m.nsensordata):
raise ValueError('models are not compatible')
# check trailing dimensions
_check_trailing_dimension(nstate, initial_state=initial_state, state=state)
_check_trailing_dimension(ncontrol, control=control)
_check_trailing_dimension(nv, initial_warmstart=initial_warmstart)
_check_trailing_dimension(nsensordata, sensordata=sensordata)
# tile input arrays/lists if required (singleton expansion)
model = model*nroll if len(model) == 1 else model
initial_state = _tile_if_required(initial_state, nroll) initial_state = _tile_if_required(initial_state, nroll)
initial_warmstart = _tile_if_required(initial_warmstart, nroll) initial_warmstart = _tile_if_required(initial_warmstart, nroll)
control = _tile_if_required(control, nroll, nstep) control = _tile_if_required(control, nroll, nstep)
@@ -139,7 +160,7 @@ def rollout(model: mujoco.MjModel,
if state is None: if state is None:
state = np.empty((nroll, nstep, nstate)) state = np.empty((nroll, nstep, nstate))
if sensordata is None: if sensordata is None:
sensordata = np.empty((nroll, nstep, model.nsensordata)) sensordata = np.empty((nroll, nstep, nsensordata))
# call rollout # call rollout
_rollout.rollout(model, data, nstep, control_spec, initial_state, _rollout.rollout(model, data, nstep, control_spec, initial_state,
+37 -4
View File
@@ -334,6 +334,34 @@ class MuJoCoRolloutTest(parameterized.TestCase):
np.testing.assert_array_equal(state, py_state) np.testing.assert_array_equal(state, py_state)
np.testing.assert_array_equal(sensordata, py_sensordata) np.testing.assert_array_equal(sensordata, py_sensordata)
@parameterized.parameters(ALL_MODELS.keys())
def test_multi_model(self, model_name):
nroll = 3 # number of initial states and models
nstep = 3 # number of timesteps
spec = mujoco.MjSpec.from_string(ALL_MODELS[model_name])
if len(spec.bodies) > 1:
model = []
for i in range(nroll):
body = spec.bodies[1]
assert body.name != 'world'
body.pos = body.pos + i
model.append(spec.compile())
else:
model = [spec.compile() for i in range(nroll)]
nstate = mujoco.mj_stateSize(model[0], mujoco.mjtState.mjSTATE_FULLPHYSICS)
data = mujoco.MjData(model[0])
initial_state = np.random.randn(nroll, nstate)
control = np.random.randn(nroll, nstep, model[0].nu)
state, sensordata = rollout.rollout(model, data, initial_state, control)
py_state, py_sensordata = py_rollout(model, data, initial_state, control)
np.testing.assert_array_equal(state, py_state)
np.testing.assert_array_equal(sensordata, py_sensordata)
@parameterized.parameters(ALL_MODELS.keys()) @parameterized.parameters(ALL_MODELS.keys())
def test_multi_rollout_fixed_ctrl_infer_from_output(self, model_name): def test_multi_rollout_fixed_ctrl_infer_from_output(self, model_name):
model = mujoco.MjModel.from_xml_string(ALL_MODELS[model_name]) model = mujoco.MjModel.from_xml_string(ALL_MODELS[model_name])
@@ -430,8 +458,9 @@ class MuJoCoRolloutTest(parameterized.TestCase):
def thread_initializer(): def thread_initializer():
thread_local.data = mujoco.MjData(model) thread_local.data = mujoco.MjData(model)
model_list = [model]*nroll
def call_rollout(initial_state, control, state, sensordata): def call_rollout(initial_state, control, state, sensordata):
rollout.rollout(model, thread_local.data, initial_state, control, rollout.rollout(model_list, thread_local.data, initial_state, control,
skip_checks=True, skip_checks=True,
nstep=nstep, state=state, sensordata=sensordata) nstep=nstep, state=state, sensordata=sensordata)
@@ -677,13 +706,17 @@ def py_rollout(model, data, initial_state, control,
control = ensure_3d(control) control = ensure_3d(control)
nroll = initial_state.shape[0] nroll = initial_state.shape[0]
nstep = control.shape[1] nstep = control.shape[1]
nstate = mujoco.mj_stateSize(model, mujoco.mjtState.mjSTATE_FULLPHYSICS)
if isinstance(model, mujoco.MjModel):
model = [model]*nroll
nstate = mujoco.mj_stateSize(model[0], mujoco.mjtState.mjSTATE_FULLPHYSICS)
state = np.empty((nroll, nstep, nstate)) state = np.empty((nroll, nstep, nstate))
sensordata = np.empty((nroll, nstep, model.nsensordata)) sensordata = np.empty((nroll, nstep, model[0].nsensordata))
for r in range(nroll): for r in range(nroll):
state_r, sensordata_r = one_rollout( state_r, sensordata_r = one_rollout(
model, data, initial_state[r], control[r], control_spec model[r], data, initial_state[r], control[r], control_spec
) )
state[r] = state_r state[r] = state_r
sensordata[r] = sensordata_r sensordata[r] = sensordata_r