You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On the Newton backend, observations computed after an in-step reset (timeout / termination / manual reset handled inside ManagerBasedRLEnv.step()) are built from pre-reset forward kinematics. Any ray-cast sensor attached to an articulation body (RayCaster, RayCasterCamera, and subclasses) publishes its pose — and therefore casts its rays / renders its depth image — from the body transform of the previous episode's final pose. The first observation of every episode is sampled metres away from where the robot actually is.
The direct reset() / reset_to() paths are fine; only the two reset branches inside step() are affected. DirectRLEnv.step() has the same gap.
ManagerBasedRLEnv.step() (source/isaaclab/isaaclab/envs/manager_based_rl_env.py, ~L270 and ~L282): after self._reset_idx(reset_env_ids) and after self._reset_idx(manual_reset_ids) it goes straight to command_manager.compute() → observation_manager.compute(). There is no scene.write_data_to_sim() / sim.forward() in between.
ManagerBasedEnv.reset() and reset_to() (source/isaaclab/isaaclab/envs/manager_based_env.py) do call self.scene.write_data_to_sim(); self.sim.forward() right after _reset_idx, with the comment # update articulation kinematics — so the requirement is known, and step() just misses it.
The asset data layer protects itself: ArticulationData._ensure_fk_fresh() (source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py, ~L131) lazily calls SimulationManager.forward() when a pose-dependent property is read after a manual write. That is why robot.data.* looks fresh after the reset.
The sensor pose path does not: _NewtonRayCasterPoseMixin.get_world_poses() (source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py, ~L168) reads the body transforms straight out of NewtonManager.get_state_0() with a warp kernel, no lazy-FK guard. NewtonManager.forward() (source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py, ~L577) documents that body poses are only recomputed from joint coordinates by forward() or the next step.
So within the reset step the articulation data and the sensors disagree about where the robot is, and the observation the policy gets as the first observation of the new episode mixes post-reset proprioception with a pre-reset height scan / depth image.
Steps to reproduce
Stock task, stock sensor, no Isaac Sim needed. A 1 s episode forces a timeout reset inside step() every 50 control steps; the reset event jitters the spawn by ±1 m so the pre- and post-reset poses are far apart. After each reset step we compare height_scanner.data.pos_w (xy) with the robot base (the scanner's offset is z-only, so the normal value is 0).
repro_stale_reset.py
"""Minimal repro: in-step resets leave ray-cast sensor poses at the PRE-reset body transforms.Stock task, Newton backend, no Isaac Sim. A 1 s episode forces a timeout reset inside`ManagerBasedRLEnv.step()` every 50 control steps; we compare the height_scanner'spublished pose against the robot base right after each in-step reset.Run: python repro_stale_reset.py (stock behaviour -> hundreds of mm stale) python repro_stale_reset.py --patched (forward() after _reset_idx -> fixed)"""importargparseimportsysimportnumpyasnpdefmain() ->None:
fromisaaclab.appimportadd_launcher_argsfromisaaclab_tasks.utilsimportsetup_preset_cliparser=argparse.ArgumentParser()
parser.add_argument("--patched", action="store_true")
parser.add_argument("--steps", type=int, default=160)
add_launcher_args(parser)
args, remaining=setup_preset_cli(parser, agent_library="rsl_rl")
sys.argv= [sys.argv[0]] +remainingimportgymnasiumasgymimporttorchfromisaaclab.appimportlaunch_simulationfromisaaclab.envsimportManagerBasedRLEnvfromisaaclab_newton.physicsimportNewtonCfgfromisaaclab_tasks.utilsimportresolve_task_configtask_id="Isaac-Velocity-Rough-G1"env_cfg, _=resolve_task_config(task_id, "rsl_rl_cfg_entry_point", play_mode=True)
ifenv_cfg.sim.physicsisNone:
env_cfg.sim.physics=NewtonCfg()
env_cfg.scene.num_envs=1env_cfg.curriculum=Noneenv_cfg.episode_length_s=1.0# timeout reset inside step() every 50 control stepsenv_cfg.terminations.base_contact=None# Large reset jitter so the pre- and post-reset base poses are far apart.env_cfg.events.reset_base.params["pose_range"]["x"] = (-1.0, 1.0)
env_cfg.events.reset_base.params["pose_range"]["y"] = (-1.0, 1.0)
ifargs.patched:
orig=ManagerBasedRLEnv._reset_idxdef_reset_idx(self, env_ids):
orig(self, env_ids)
# What reset()/reset_to() already do after _reset_idx, and step() does not.self.scene.write_data_to_sim()
self.sim.forward()
ManagerBasedRLEnv._reset_idx=_reset_idxwithlaunch_simulation(env_cfg, args):
env=gym.make(task_id, cfg=env_cfg).unwrappedrobot=env.scene["robot"]
scanner=env.scene["height_scanner"]
env.reset()
t=lambdav: v.torchifhasattr(v, "torch") elsevaction=torch.zeros((1, env.action_space.shape[-1]), device=env.device)
withtorch.inference_mode():
forkinrange(args.steps):
_, _, terminated, truncated, _=env.step(action)
ifbool((terminated|truncated).any()):
base=t(robot.data.root_link_pos_w)[0, :2].cpu().numpy()
scan=t(scanner.data.pos_w)[0, :2].cpu().numpy()
print(f"step {k+1:>4} (in-step reset): |height_scanner.pos_w - base| = "f"{np.linalg.norm(scan-base) *1000:8.1f} mm", flush=True)
env.close()
if__name__=="__main__":
main()
$ python repro_stale_reset.py
step 50 (in-step reset): |height_scanner.pos_w - base| = 1320.9 mm
step 100 (in-step reset): |height_scanner.pos_w - base| = 2244.5 mm
step 150 (in-step reset): |height_scanner.pos_w - base| = 1647.2 mm
$ python repro_stale_reset.py --patched # forward() after _reset_idx, as reset() does
step 50 (in-step reset): |height_scanner.pos_w - base| = 0.0 mm
step 100 (in-step reset): |height_scanner.pos_w - base| = 0.0 mm
step 150 (in-step reset): |height_scanner.pos_w - base| = 0.0 mm
The --patched variant monkeypatches ManagerBasedRLEnv._reset_idx to append self.scene.write_data_to_sim(); self.sim.forward() — i.e. exactly what reset() already does.
Impact on the stock tasks: every core/velocity task attaches height_scanner = RayCasterCfg(prim_path="{ENV_REGEX_NS}/Robot/base", ...), so on Newton the first height scan of every training episode is taken around the previous episode's final position. For a per-step-updating RayCaster only that one observation is wrong; for a ray-cast camera with a refresh schedule and a history/backfill buffer the stale frame is copied into the whole history stack and persists until the next scheduled refresh (in our 10 Hz depth-camera setup that is up to ~0.8 s of policy input per episode).
How we found it: the same policy walked down stairs markedly worse in our Isaac Lab port than in the mjlab original, and the divergence traced to episode starts. mjlab's step() calls sim.forward() (and re-senses) between _reset_idx and the observation compute, which is the behaviour the patch above restores.
Suggested fix (either): call self.scene.write_data_to_sim(); self.sim.forward() after both _reset_idx calls in ManagerBasedRLEnv.step() (and DirectRLEnv.step()), mirroring reset(); or give _NewtonRayCasterPoseMixin.get_world_poses() the same lazy-FK guard the asset data layer uses.
Checklist
I have checked that there is no similar issue in the repo (required)
I have checked that the issue is not in running Isaac Sim itself and is related to the repo
Acceptance Criteria
After an in-step reset, ray-cast sensor poses (RayCaster.data.pos_w, RayCasterCamera.data.pos_w) match the post-reset body transforms in the observation returned by that step() call (the repro prints 0.0 mm without the monkeypatch).
Same for DirectRLEnv.step().
A regression test covering "sensor pose after in-step reset" on the Newton backend.
Describe the bug
On the Newton backend, observations computed after an in-step reset (timeout / termination / manual reset handled inside
ManagerBasedRLEnv.step()) are built from pre-reset forward kinematics. Any ray-cast sensor attached to an articulation body (RayCaster,RayCasterCamera, and subclasses) publishes its pose — and therefore casts its rays / renders its depth image — from the body transform of the previous episode's final pose. The first observation of every episode is sampled metres away from where the robot actually is.The direct
reset()/reset_to()paths are fine; only the two reset branches insidestep()are affected.DirectRLEnv.step()has the same gap.Mechanism (commit 3c0694c):
ManagerBasedRLEnv.step()(source/isaaclab/isaaclab/envs/manager_based_rl_env.py, ~L270 and ~L282): afterself._reset_idx(reset_env_ids)and afterself._reset_idx(manual_reset_ids)it goes straight tocommand_manager.compute()→observation_manager.compute(). There is noscene.write_data_to_sim()/sim.forward()in between.ManagerBasedEnv.reset()andreset_to()(source/isaaclab/isaaclab/envs/manager_based_env.py) do callself.scene.write_data_to_sim(); self.sim.forward()right after_reset_idx, with the comment# update articulation kinematics— so the requirement is known, andstep()just misses it.ArticulationData._ensure_fk_fresh()(source/isaaclab_newton/isaaclab_newton/assets/articulation/articulation_data.py, ~L131) lazily callsSimulationManager.forward()when a pose-dependent property is read after a manual write. That is whyrobot.data.*looks fresh after the reset._NewtonRayCasterPoseMixin.get_world_poses()(source/isaaclab_newton/isaaclab_newton/sensors/ray_caster/newton_raycast_sensor.py, ~L168) reads the body transforms straight out ofNewtonManager.get_state_0()with a warp kernel, no lazy-FK guard.NewtonManager.forward()(source/isaaclab_newton/isaaclab_newton/physics/newton_manager.py, ~L577) documents that body poses are only recomputed from joint coordinates byforward()or the nextstep.So within the reset step the articulation data and the sensors disagree about where the robot is, and the observation the policy gets as the first observation of the new episode mixes post-reset proprioception with a pre-reset height scan / depth image.
Steps to reproduce
Stock task, stock sensor, no Isaac Sim needed. A 1 s episode forces a timeout reset inside
step()every 50 control steps; the reset event jitters the spawn by ±1 m so the pre- and post-reset poses are far apart. After each reset step we compareheight_scanner.data.pos_w(xy) with the robot base (the scanner's offset is z-only, so the normal value is 0).repro_stale_reset.pyThe
--patchedvariant monkeypatchesManagerBasedRLEnv._reset_idxto appendself.scene.write_data_to_sim(); self.sim.forward()— i.e. exactly whatreset()already does.System Info
newton1.5.0,mujoco-warp3.11.0,warp-lang1.16.0)Additional context
core/velocitytask attachesheight_scanner = RayCasterCfg(prim_path="{ENV_REGEX_NS}/Robot/base", ...), so on Newton the first height scan of every training episode is taken around the previous episode's final position. For a per-step-updatingRayCasteronly that one observation is wrong; for a ray-cast camera with a refresh schedule and a history/backfill buffer the stale frame is copied into the whole history stack and persists until the next scheduled refresh (in our 10 Hz depth-camera setup that is up to ~0.8 s of policy input per episode).step()callssim.forward()(and re-senses) between_reset_idxand the observation compute, which is the behaviour the patch above restores.reset()with fabric enabled (render-side transforms). This report is about the Newton backend, ray-cast sensors, and the in-step reset path;reset()itself is correct here.self.scene.write_data_to_sim(); self.sim.forward()after both_reset_idxcalls inManagerBasedRLEnv.step()(andDirectRLEnv.step()), mirroringreset(); or give_NewtonRayCasterPoseMixin.get_world_poses()the same lazy-FK guard the asset data layer uses.Checklist
Acceptance Criteria
RayCaster.data.pos_w,RayCasterCamera.data.pos_w) match the post-reset body transforms in the observation returned by thatstep()call (the repro prints 0.0 mm without the monkeypatch).DirectRLEnv.step().