From bf48375d2f1b0811d61d8c3b3fa6bfbb1cb1373f Mon Sep 17 00:00:00 2001 From: Tobias Juelg Date: Tue, 20 Oct 2026 21:33:44 -0700 Subject: [PATCH 1/2] feat(sim): configurable MuJoCo arena size in SimConfig MuJoCo sizes a model's arena (stack + constraint memory) from the model at rest. Scenes that get many contacts during an episode, e.g. DuoBench's ball_maze with ~170 contacts and ~1100 constraint rows, can exceed it and abort with "mj_stackAlloc: insufficient memory". Add SimConfig.memory (bytes, -1 = keep the model's or MuJoCo's automatic sizing). Sim applies it to the composer's spec before compiling, since the arena size is fixed at compile time; a in attached object XMLs is ignored because MjSpec.attach keeps the parent's options. --- python/rcs/_core/sim.pyi | 2 ++ python/rcs/sim/sim.py | 3 +++ src/pybind/rcs.cpp | 7 +++++-- src/sim/sim.h | 5 +++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/python/rcs/_core/sim.pyi b/python/rcs/_core/sim.pyi index 042e2608e..7f6a21d23 100644 --- a/python/rcs/_core/sim.pyi +++ b/python/rcs/_core/sim.pyi @@ -140,6 +140,7 @@ class SimConfig: async_control: bool frequency: float max_convergence_steps: int + memory: int realtime: bool def __copy__(self) -> SimConfig: ... def __deepcopy__(self, arg0: dict) -> SimConfig: ... @@ -149,6 +150,7 @@ class SimConfig: realtime: bool = False, frequency: float = 30.0, max_convergence_steps: int = 500, + memory: int = -1, ) -> None: ... class SimGripper(rcs._core.common.Gripper): diff --git a/python/rcs/sim/sim.py b/python/rcs/sim/sim.py index 4ee8b02bc..86df66ac5 100644 --- a/python/rcs/sim/sim.py +++ b/python/rcs/sim/sim.py @@ -65,6 +65,9 @@ def gui_loop(gui_uuid: str, close_event): class Sim(_Sim): def __init__(self, mjmdl: str | PathLike | ModelComposer, cfg: SimConfig | None = None): if isinstance(mjmdl, ModelComposer): + # the arena size is fixed at compile time + if cfg is not None and cfg.memory >= 0: + mjmdl.spec.memory = cfg.memory self.model = mjmdl.get_model() else: mjmdl = Path(mjmdl) diff --git a/src/pybind/rcs.cpp b/src/pybind/rcs.cpp index 14616a41d..767c83a1b 100644 --- a/src/pybind/rcs.cpp +++ b/src/pybind/rcs.cpp @@ -751,24 +751,27 @@ PYBIND11_MODULE(_core, m) { rcs::sim::SimConfig default_sim_cfg = rcs::sim::SimConfig(); py::class_(sim, "SimConfig") .def(py::init([](bool async_control, bool realtime, double frequency, - int max_convergence_steps) { + int max_convergence_steps, long long memory) { rcs::sim::SimConfig config; config.async_control = async_control; config.realtime = realtime; config.frequency = frequency; config.max_convergence_steps = max_convergence_steps; + config.memory = memory; return config; }), py::arg("async_control") = default_sim_cfg.async_control, py::arg("realtime") = default_sim_cfg.realtime, py::arg("frequency") = default_sim_cfg.frequency, py::arg("max_convergence_steps") = - default_sim_cfg.max_convergence_steps) + default_sim_cfg.max_convergence_steps, + py::arg("memory") = default_sim_cfg.memory) .def_readwrite("async_control", &rcs::sim::SimConfig::async_control) .def_readwrite("realtime", &rcs::sim::SimConfig::realtime) .def_readwrite("frequency", &rcs::sim::SimConfig::frequency) .def_readwrite("max_convergence_steps", &rcs::sim::SimConfig::max_convergence_steps) + .def_readwrite("memory", &rcs::sim::SimConfig::memory) .def("__copy__", [](const rcs::sim::SimConfig& self) { return rcs::sim::SimConfig(self); diff --git a/src/sim/sim.h b/src/sim/sim.h index 6cae264c0..9bebea9b0 100644 --- a/src/sim/sim.h +++ b/src/sim/sim.h @@ -34,6 +34,11 @@ struct SimConfig { bool realtime = false; double frequency = 30; // in Hz int max_convergence_steps = 500; + // MuJoCo arena (stack + constraint) size in bytes, applied when a scene is + // compiled from a ModelComposer. -1 keeps the model's or + // MuJoCo's automatic sizing, which can be too small for scenes with many + // contacts ("mj_stackAlloc: insufficient memory"). + long long memory = -1; }; struct Callback { From ead2733110650ddaa5fefeeb8e862b4dcf1fa5ae Mon Sep 17 00:00:00 2001 From: Tobias Juelg Date: Tue, 20 Oct 2026 21:33:44 -0700 Subject: [PATCH 2/2] test(sim): SimConfig.memory sets the compiled arena size --- python/tests/test_sim_envs.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/python/tests/test_sim_envs.py b/python/tests/test_sim_envs.py index 4a8428107..a9e788a37 100644 --- a/python/tests/test_sim_envs.py +++ b/python/tests/test_sim_envs.py @@ -328,3 +328,17 @@ def test_relative_zero_action_joints(self): act.update(GripperDictType(gripper=np.array([1.0]))) obs, _, _, _, info = env.step(act) self.assert_no_pose_change(info, obs_initial, obs) + + +def test_sim_config_memory_sets_arena_size(): + scene = EmptyWorldFR3() + cfg = copy.deepcopy(scene.config()) + cfg.headless = True + prefixed_cfg = scene.prefixed_cfg(cfg) + assert prefixed_cfg.sim_cfg.memory == -1 + default = sim.Sim(scene.create_model(prefixed_cfg), prefixed_cfg.sim_cfg) + + prefixed_cfg.sim_cfg.memory = 64 * 1024 * 1024 + enlarged = sim.Sim(scene.create_model(prefixed_cfg), prefixed_cfg.sim_cfg) + assert enlarged.model.narena == 64 * 1024 * 1024 + assert default.model.narena < enlarged.model.narena