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
Hi, I'm working on Brax for my master thesis in Safe Reinforcement Learning, where I want to integrate common tasks into Brax. One of these tasks is for the agent to walk to a goal while avoiding hazards. I would like the goal to change position after the agent reaches it. However, I'm struggling to get it actually rendering the new position of the goal.
I've created various environments, but for testing purposes, I'm trying to make it work with this environment where I set a new position for the goal every 10 steps.
importjaximportjax.numpyasjpimportmujocoimportosfrombraximportbasefrombraximportenvsfrombrax.baseimportStateasPipelineStatefrombrax.envs.baseimportEnv, PipelineEnv, Statefrombrax.mjx.baseimportStateasMjxStatefrombrax.ioimportmjcffromtypingimportTupleclassSimpleResettingGoal(PipelineEnv):
"""Environment with goal respawning every 10 steps."""def__init__(
self,
forward_reward_weight=1.25,
ctrl_cost_weight=0.1,
healthy_reward=5.0,
terminate_when_unhealthy=True,
healthy_z_range=(1.0, 2.0),
reset_noise_scale=1e-2,
exclude_current_positions_from_observation=True,
goal_reset_steps=10, # Parameter for goal reset frequencygoal_area=(-5.0, -5.0, 5.0, 5.0), # xmin, ymin, xmax, ymax**kwargs,
):
# Get XML pathcurrent_dir=os.path.dirname(os.path.abspath(__file__))
xml_path=os.path.join(os.path.dirname(current_dir), 'xml', 'humanoid_hazard_goal.xml')
mj_model=mujoco.MjModel.from_xml_path(xml_path)
mj_model.opt.solver=mujoco.mjtSolver.mjSOL_CGmj_model.opt.iterations=6mj_model.opt.ls_iterations=6sys=mjcf.load_model(mj_model)
physics_steps_per_control_step=5kwargs['n_frames'] =kwargs.get(
'n_frames', physics_steps_per_control_step)
kwargs['backend'] ='mjx'super().__init__(sys, **kwargs)
self._forward_reward_weight=forward_reward_weightself._ctrl_cost_weight=ctrl_cost_weightself._healthy_reward=healthy_rewardself._terminate_when_unhealthy=terminate_when_unhealthyself._healthy_z_range=healthy_z_rangeself._reset_noise_scale=reset_noise_scaleself._exclude_current_positions_from_observation= (
exclude_current_positions_from_observation
)
self._goal_body=mj_model.body('goal').idself._hazard_body_1=mj_model.body('hazard1').idself._hazard_body_2=mj_model.body('hazard2').idself._hazard_body_3=mj_model.body('hazard3').idself.goal_reset_steps=goal_reset_steps# Store the reset frequencyself.goal_area=goal_area# Store goal areadef_generate_random_goal_position(self, rng):
"""Generate a random goal position within the defined area."""xmin, ymin, xmax, ymax=self.goal_areagoal_x=jax.random.uniform(rng, (), minval=xmin, maxval=xmax)
goal_y=jax.random.uniform(rng, (), minval=ymin, maxval=ymax)
goal_pos=jp.array([goal_x, goal_y, 1.0]) # Fixed z-coordinatereturngoal_posdefreset(self, rng: jp.ndarray) ->State:
"""Resets the environment to an initial state."""rng, rng1, rng2, rng3=jax.random.split(rng, 4)
low, hi=-self._reset_noise_scale, self._reset_noise_scaleqpos=self.sys.qpos0+jax.random.uniform(
rng1, (self.sys.nq,), minval=low, maxval=hi
)
qvel=jax.random.uniform(
rng2, (self.sys.nv,), minval=low, maxval=hi
)
# Initial placement of the goal (using _generate_random_goal_position)goal_pos=self._generate_random_goal_position(rng3)
pipeline_state=self.pipeline_init(qpos, qvel)
pipeline_state=pipeline_state.replace(
xpos=pipeline_state.xpos.at[self._goal_body].set(goal_pos) # Set initial goal position.
)
obs=self._get_obs(pipeline_state)
reward, done, zero=jp.zeros(3)
metrics= {
'reward_forward': zero,
'reward_ctrl': zero,
'reward_healthy': zero,
'x_position': zero,
'y_position': zero,
}
info= {
'goal_pos': goal_pos, # Store for later use.'steps': 0,
'goal_resets': 0
} # Initialize step counterreturnState(pipeline_state, obs, reward, done, metrics, info)
defstep(self, state: State, action: jp.ndarray) ->State:
"""Run one timestep of the environment's dynamics."""data=self.pipeline_step(state.pipeline_state, action)
# --- Goal Reset Logic ---goal_pos=state.info['goal_pos']
steps=state.info['steps'] +1goal_resets=state.info['goal_resets']
rng=jax.random.PRNGKey(steps)
should_reset_goal= (steps%self.goal_reset_steps) ==0deftrue_fn(_):
new_rng, rng1=jax.random.split(rng)
new_goal_pos=self._generate_random_goal_position(rng1)
new_data=data.replace(xpos=data.xpos.at[self._goal_body].set(new_goal_pos))
returnnew_rng, new_data, new_goal_pos, goal_resets+1deffalse_fn(_):
returnrng, data, goal_pos, goal_resets# Apply the conditionalrng, data, goal_pos, goal_resets=jax.lax.cond(
should_reset_goal,
true_fn,
false_fn,
None# Dummy operand that gets passed to both branches
)
# --- Reward Calculation (Example) ---# Replace with your actual reward/cost functionreward=1.0# Placeholder: Give a small reward for existing.done=jp.array(0, dtype=jp.float32)
# --- Observation ---obs=self._get_obs(data)
# --- Update State ---new_info=state.info.copy()
new_info.update({
'goal_pos': goal_pos,
'steps': steps,
'goal_resets': goal_resets
}) # Update step count in inforeturnstate.replace(
pipeline_state=data, obs=obs, reward=reward, done=done, info=new_info
)
def_get_obs(self, data: MjxState) ->jp.ndarray:
"""Return relevant observations."""# Example: Include goal position in observation. Adapt as needed.qpos=data.qposqvel=data.qvel# Get the goal position from the bodygoal_pos=data.xpos[self._goal_body, :2]
# Get the humanoid position from the comhumanoid_pos=data.xpos[1, :2]
# Relative positionrelative_pos=goal_pos-humanoid_pos# Concatenate position and velocity of the humanoid, and add the relative positionobs=jp.concatenate([qpos[2:], qvel, relative_pos]) # Exclude root z-height.returnobs
I then use this code to render the rollout:
# instantiate the environmentenv_name='SimpleResettingGoal'env=envs.get_environment(env_name)
# define the jit reset/step functionsjit_reset=jax.jit(env.reset)
jit_step=jax.jit(env.step)
# initialize the statestate=jit_reset(jax.random.PRNGKey(0))
rollout= [state.pipeline_state] # store the pipeline_state for rendering# grab a trajectoryfirst=Trueforiinrange(200):
ctrl=-0.1*jp.ones(env.sys.nu)
state=jit_step(state, ctrl)
rollout.append(state.pipeline_state) # append pipeline_state, not whole statefirst=Falsemedia.show_video(env.render(rollout, camera='side'), fps=1.0/env.dt)
In the video, the goal just stays at the initial position given in the xml. Please let me know how I can manage to do this and whether I need to provide any more information
Hi @mrdbstn , if you are using the MJX backend, it is not sufficient to update xpos. One must update qpos or mocap_pos, since xpos is derived from qpos. I recommend using mocap_pos as in this example. Let us know if you have more questions.
Hi @btaba, thanks for the reply! It works, I can move the goal successfully now by hard-coding the mocap_pos. My question now is that I am setting the mocap_pos of the goal to a random position in the reset function, it seems to be getting set to exact same value even across runs. I figures this is what domain_randomization is supposed to be used for, but as far as I understand I only have access to sys, but mocap_pos lives in data. How am I supposed to randomize the goal position so the agent learns a more generalizable policy?
fromdatetimeimportdatetimefrometilsimportepathimportfunctoolsfromIPython.displayimportHTMLfromtypingimportAny, Dict, Sequence, Tuple, Union, Optionalimportosfromml_collectionsimportconfig_dictimportjaxfromjaximportnumpyasjpimportnumpyasnpfromflax.trainingimportorbax_utilsfromflaximportstructfrommatplotlibimportpyplotaspltimportmediapyasmediafromorbaximportcheckpointasocpimportmujocofrommujocoimportmjxfrombraximportbasefrombraximportenvsfrombraximportmathfrombrax.baseimportBase, Motion, Transformfrombrax.baseimportStateasPipelineStatefrombrax.envs.baseimportEnv, PipelineEnv, Statefrombrax.mjx.baseimportStateasMjxStatefrombrax.training.agents.ppoimporttrainasppofrombrax.training.agents.ppoimportnetworksasppo_networksfrombrax.ioimporthtml, mjcf, model# Path to car_hazard_goal.xml - adjust if neededCAR_HAZARD_GOAL_XML_PATH='xml/car_hazard_goal.xml'defdefault_config() ->config_dict.ConfigDict:
"""Returns the default config for CarHazardGoal environment."""config=config_dict.create(
# New safety-gymnasium reward parametersreward_distance=1.0, # Dense reward scale for distance moved to the goalreward_goal=1.0, # Sparse reward for reaching the goalgoal_size=0.7, # Distance threshold for achieving the goalreward_orientation=False, # Optional: Reward for maintaining upright orientationreward_orientation_scale=0.002, # Scale for orientation rewardreward_orientation_body='agent', # Body to check orientation (unused if reward_orientation=False)# Other parameters (kept or adjusted)terminate_when_unhealthy=True, # Keep termination based on healthhealthy_z_range=(0.05, 0.3), # Keep health definitionreset_noise_scale=0.005,
exclude_current_positions_from_observation=True,
max_velocity=5.0, # Keep velocity limit for calculation stabilitydebug=False,
)
returnconfigdefsafe_norm(x, axis=None, keepdims=False, eps=1e-8):
"""Safely compute the norm with a small epsilon to avoid NaN."""returnjp.sqrt(jp.sum(jp.square(x), axis=axis, keepdims=keepdims) +eps)
# Add JAX-compatible helper function for NaN handlingdefnan_to_zero(x):
"""Replace NaN values with zeros in a JAX-compatible way."""returnjp.where(jp.isnan(x), jp.zeros_like(x), x)
classCarHazardGoal(PipelineEnv):
def__init__(
self,
config: config_dict.ConfigDict=default_config(),
config_overrides: Optional[Dict[str, Any]] =None,
**kwargs,
):
# Load the car model from XMLmj_model=mujoco.MjModel.from_xml_path(CAR_HAZARD_GOAL_XML_PATH)
mj_model.opt.solver=mujoco.mjtSolver.mjSOL_CGmj_model.opt.iterations=4mj_model.opt.ls_iterations=4# Get body IDs directly from MuJoCo model before loading into Braxtry:
self._agent_body_name='agent'# Store name for orientation reward if neededself._agent_body=mj_model.body(self._agent_body_name).idself._goal_body=mj_model.body('goal').id# Still useful for observation? Or use mocap? Mocap is used later.# Get hazard body IDs (kept for potential future use, but not reward)self._hazard_bodies= []
foriinrange(1, 5):
hazard_name=f'hazard{i}'try:
self._hazard_bodies.append(mj_model.body(hazard_name).id)
exceptException:
pass# Skip if hazard doesn't existexceptExceptionase:
print(f"Warning: Error getting body IDs: {e}")
# Fallback to index-based approachself._agent_body_name='agent'self._agent_body=1self._goal_body=2self._hazard_bodies= [3, 4, 5, 6]
sys=mjcf.load_model(mj_model)
# Find the mocap ID for the goal bodyself._goal_mocap_id=Noneifmj_model.nmocap>0:
foriinrange(mj_model.nbody):
ifmj_model.body(i).name=="goal"andmj_model.body(i).mocapid>=0:
print(f"Goal mocap ID found: {mj_model.body(i).mocapid}")
self._goal_mocap_id=mj_model.body(i).mocapidbreakelse:
# Fallback if 'goal' mocap body not found by nameifmj_model.nmocap>0:
print("Warning: 'goal' body with mocapid >= 0 not found. Defaulting to mocapid 0.")
self._goal_mocap_id=0else:
print("Error: No mocap bodies found in the model.")
# Or raise an error, depending on requirementsself._goal_mocap_id=-1# Indicate error or invalid stateelse:
print("Error: No mocap bodies defined in the model.")
self._goal_mocap_id=-1# Indicate error or invalid statephysics_steps_per_control_step=4kwargs['n_frames'] =kwargs.get(
'n_frames', physics_steps_per_control_step)
kwargs['backend'] ='mjx'super().__init__(sys, **kwargs)
# Apply config overrides if providedifconfig_overrides:
config=config.copy_and_resolve_references()
fork, vinconfig_overrides.items():
config[k] =vself._config=config# Store new reward parametersself._reward_distance=config.reward_distanceself._reward_goal=config.reward_goalself._goal_size=config.goal_sizeself._reward_orientation=config.reward_orientationself._reward_orientation_scale=config.reward_orientation_scaleself._reward_orientation_body=config.reward_orientation_body# Name stored# Store other necessary parametersself._terminate_when_unhealthy=config.terminate_when_unhealthyself._healthy_z_range=config.healthy_z_rangeself._reset_noise_scale=config.reset_noise_scaleself._exclude_current_positions_from_observation= (
config.exclude_current_positions_from_observation
)
self._max_velocity=config.max_velocityself._debug=config.debugdefreset(self, rng: jp.ndarray) ->State:
"""Resets the environment to an initial state with randomized goal."""rng, rng1, rng2, rng_goal=jax.random.split(rng, 4)
# Randomize initial car position with small noiselow, hi=-self._reset_noise_scale, self._reset_noise_scaleqpos=self.sys.qpos0+jax.random.uniform(
rng1, (self.sys.nq,), minval=low, maxval=hi
)
qvel=jax.random.uniform(
rng2, (self.sys.nv,), minval=low, maxval=hi
)
# Ensure qpos has valid quaternionifqpos.shape[0] >6: # If quaternion existsquat_norm=safe_norm(qpos[3:7])
qpos=qpos.at[3:7].set(qpos[3:7] /quat_norm)
# Initialize environmentdata=self.pipeline_init(qpos, qvel)
# Randomize goal position (with more moderate bounds)goal_pos=jax.random.uniform(
rng_goal,
(3,),
minval=jp.array([-2.0, -2.0, 0.09]),
maxval=jp.array([2.0, 2.0, 0.09]),
)
# Get positionsagent_pos=data.xpos[self._agent_body]
# Set goal position in the identified mocap body# Add check to ensure _goal_mocap_id is validifself._goal_mocap_idisnotNoneandself._goal_mocap_id>=0:
data=data.replace(mocap_pos=data.mocap_pos.at[self._goal_mocap_id].set(goal_pos))
print(f"Goal position set to: {goal_pos}")
else:
print("Error: Invalid or missing goal mocap ID. Cannot set goal position.")
# Handle error case: maybe set done=True or raise exception# For now, we proceed but the goal won't be set correctlypass# Calculate initial distance to goal for the first stepinitial_dist_goal=safe_norm(agent_pos[:2] -goal_pos[:2])
# Store goal position and initial distance for reward calculationinfo= {
"goal_pos": goal_pos,
"step_count": 0,
"last_obs": None,
"last_dist_goal": initial_dist_goal, # Initialize last_dist_goal
}
# Get observationobs=self._get_obs(data)
# Store observation in info for stability checkinfo["last_obs"] =obsreward, done, zero=jp.zeros(3)
metrics= {
# New reward components'dist_reward': zero, # Reward from distance change'goal_reward': zero, # Reward for reaching goal'orientation_reward': zero, # Reward for orientation (if enabled)'reward': zero, # Total reward (expected by wrappers)# Other potentially useful metrics'x_position': jp.array(data.xpos[self._agent_body, 0]),
'y_position': jp.array(data.xpos[self._agent_body, 1]),
'distance_to_goal': initial_dist_goal,
'x_velocity': zero,
'y_velocity': zero,
'z_alignment': zero, # Z-alignment metric (even if reward disabled)
}
returnState(data, obs, reward, done, metrics, info)
defstep(self, state: State, action: jp.ndarray) ->State:
"""Runs one timestep of the environment's dynamics."""# Increment step counterstep_count=state.info.get('step_count', 0) +1# Get the last valid observation and distancelast_obs=state.info.get('last_obs', state.obs)
last_dist_goal=state.info['last_dist_goal']
# Clip action for stability (optional, can be removed if not needed)action=jp.clip(action, -1.0, 1.0)
data0=state.pipeline_statedata=self.pipeline_step(data0, action)
# Get positionsagent_pos=data.xpos[self._agent_body]
goal_pos=state.info['goal_pos']
# --- Calculate Reward Components --- # 1. Distance-Based Rewarddist_goal=safe_norm(agent_pos[:2] -goal_pos[:2])
dist_reward= (last_dist_goal-dist_goal) *self._reward_distance# 2. Goal Achievement Rewardgoal_achieved=dist_goal<=self._goal_sizegoal_reward=jp.where(goal_achieved, self._reward_goal, 0.0)
# 3. Orientation Reward (Optional)z_alignment=0.0orientation_reward=0.0ifself._reward_orientation:
# Get quaternion of the agent body (assuming it's the first part of qpos)# Check if the orientation body exists in the model# Note: This assumes the agent's quaternion is at the start of qpos after potential exclusion.# Adjust indexing if necessary based on your MJCF structure and observation exclusion.quat=data.qpos[3:7] # Assuming standard floating base: [x, y, z, qw, qx, qy, qz, ...] -> qw, qx, qy, qz indices 3-7# Normalize quaternionquat_norm=safe_norm(quat)
quat=quat/jp.maximum(quat_norm, 1e-8)
# Calculate rotation matrixrot_matrix=math.quat_to_rot(quat)
# zalign = R[2, 2]z_alignment=rot_matrix[2, 2]
orientation_reward=self._reward_orientation_scale*z_alignment# --- Total Reward --- reward=dist_reward+goal_reward+orientation_reward# --- Health Check (for termination, not reward) ---min_z, max_z=self._healthy_z_rangeis_healthy=jp.logical_and(
agent_pos[2] >=min_z,
agent_pos[2] <=max_z
).astype(jp.float32)
# --- Termination Conditions ---# Check for NaN in state using JAX-compatible operations# Calculate velocity for NaN check (keep calculation local if only needed here)dt=jp.maximum(self.dt, 1e-6) # Ensure dt is not too smallvelocity= (agent_pos-data0.xpos[self._agent_body]) /dthas_nan_pos=jp.any(jp.isnan(agent_pos))
has_nan_vel=jp.any(jp.isnan(velocity))
has_nan_state=jp.logical_or(has_nan_pos, has_nan_vel)
# Terminate if unhealthy (optional), goal reached, or NaN statedone=jp.logical_or(
jp.logical_or(
(1.0-is_healthy) *self._terminate_when_unhealthy,
goal_achieved
),
has_nan_state
)
# --- Observation and State Update ---obs=self._get_obs(data)
# Handle NaN observation using JAX-compatible operationshas_nan_obs=jp.any(jp.isnan(obs))
obs=jp.where(has_nan_obs, last_obs, obs)
# Update info dictionary - update existing dict instead of replacing# Ensure all keys from input state.info are preservednew_info=state.info.copy() # Start with a copy of the input infonew_info.update({
"goal_pos": goal_pos, # Update goal position (usually static but good practice)"step_count": step_count, # Update internal step counter if needed"last_obs": obs, # Store current observation for next step's fallback"last_dist_goal": dist_goal, # Store current distance for next step's reward calculation
})
# Update metrics safelymetrics= {
'dist_reward': dist_reward,
'goal_reward': goal_reward,
'orientation_reward': orientation_reward,
'reward': reward, # Add total reward to metrics'x_position': jp.clip(agent_pos[0], -10.0, 10.0), # Keep clipping for metric stability'y_position': jp.clip(agent_pos[1], -10.0, 10.0),
'distance_to_goal': dist_goal,
'x_velocity': jp.clip(velocity[0], -self._max_velocity, self._max_velocity),
'y_velocity': jp.clip(velocity[1], -self._max_velocity, self._max_velocity),
'z_alignment': z_alignment, # Log z_alignment
}
# Create fresh StatereturnState(data, obs, reward, done.astype(jp.float32), metrics, new_info)
def_get_obs(self, data: mjx.Data) ->jp.ndarray:
"""Creates an observation that includes car state and goal information."""agent_pos=data.xpos[self._agent_body]
# Get goal position from the identified mocap body# Add check for valid mocap IDifself._goal_mocap_idisnotNoneandself._goal_mocap_id>=0:
goal_pos=data.mocap_pos[self._goal_mocap_id]
else:
# Handle error: Use a default or raise an errorprint("Warning: Invalid or missing goal mocap ID in _get_obs. Using origin as default goal.")
goal_pos=jp.zeros(3)
position=data.qposifself._exclude_current_positions_from_observation:
position=position[2:] # Exclude global x,y position# Include relevant state information with clipping for stabilityqpos_obs=jp.clip(position, -10.0, 10.0)
qvel_obs=jp.clip(data.qvel, -10.0, 10.0)
# Relative position to goal with checksrel_goal_pos=jp.clip(goal_pos-agent_pos, -10.0, 10.0)
# Squeeze potential extra dimension from rel_goal_posrel_goal_pos=jp.squeeze(rel_goal_pos)
# Orientation - ensure it's properly normalizedifposition.shape[0] >=4: # If quaternion existsorientation=position[:4]
quat_norm=safe_norm(orientation)
orientation=orientation/jp.maximum(quat_norm, 1e-6)
else:
orientation=jp.array([1.0, 0.0, 0.0, 0.0]) # Default quaternion# Actuator forcesactuator_forces=jp.clip(data.qfrc_actuator, -10.0, 10.0)
# Build observation with JAX-compatible NaN handlingobs=jp.concatenate([
nan_to_zero(qpos_obs),
nan_to_zero(qvel_obs),
nan_to_zero(rel_goal_pos),
nan_to_zero(orientation),
nan_to_zero(actuator_forces),
])
returnobs# Register the environmentenvs.register_environment('car_hazard_goal', CarHazardGoal)
This discussion was converted from issue #585 on April 07, 2025 23:22.
Heading
Bold
Italic
Quote
Code
Link
Numbered list
Unordered list
Task list
Attach files
Mention
Reference
Menu
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Hi, I'm working on Brax for my master thesis in Safe Reinforcement Learning, where I want to integrate common tasks into Brax. One of these tasks is for the agent to walk to a goal while avoiding hazards. I would like the goal to change position after the agent reaches it. However, I'm struggling to get it actually rendering the new position of the goal.
humanoid_hazard_goal.xml
I've created various environments, but for testing purposes, I'm trying to make it work with this environment where I set a new position for the goal every 10 steps.
I then use this code to render the rollout:
In the video, the goal just stays at the initial position given in the xml. Please let me know how I can manage to do this and whether I need to provide any more information
All reactions