diff --git a/notebooks/01_observations.py b/notebooks/01_observations.py index 567cabf463..92c8c99750 100644 --- a/notebooks/01_observations.py +++ b/notebooks/01_observations.py @@ -216,7 +216,7 @@ # ## Lane / boundary segments # %% -road_labels = ["rel_x", "rel_y", "rel_z", "length", "width", "dir_cos", "dir_sin", "goal_dist_abs", "goal_dist_rel"] +road_labels = ["rel_x", "rel_y", "rel_z", "length", "dir_cos", "dir_sin", "width", "goal_dist_abs", "goal_dist_rel"] lane_active = ~np.all(lanes == 0, axis=1) bound_active = ~np.all(boundaries == 0, axis=1) @@ -228,7 +228,7 @@ # Mirror the canonical road rendering in pufferlib.viz.plot_observation for seg in lanes[lane_active]: - x, y, z, length, width, dc, ds = seg[:7] + x, y, z, length, dc, ds = seg[:6] # seg[7] = goal_dist_abs (0 near goal lane -> 1 far); green->red colormap color = plt.cm.RdYlGn_r(float(seg[7])) if env.obs_goal_lane_distance else "lightgrey" ax.scatter(x, y, color=color, s=10, zorder=1) @@ -241,7 +241,7 @@ ) for seg in boundaries[bound_active]: - x, y, z, length, width, dc, ds = seg[:7] + x, y, z, length, dc, ds = seg[:6] ax.scatter(x, y, color="black", s=10, zorder=1) ax.plot( [x + dc * length / 2, x - dc * length / 2], diff --git a/notebooks/05_inference.py b/notebooks/05_inference.py index 48337a52d8..17122e6712 100644 --- a/notebooks/05_inference.py +++ b/notebooks/05_inference.py @@ -272,8 +272,8 @@ def run_rollout(env, policy, action_selection=ACTION_SELECT_SAMPLE, horizon=HORI # - **Conditioning** (if enabled): 17 reward coefs (goal_radius, goal_speed, collision, offroad, comfort, lane_align, vel_align, lane_center, center_bias, velocity, reverse, stop_line, timestep, overspeed, throttle, steer, acc) + target waypoints # - **Target**: static=rel_x,rel_y,rel_z per waypoint; dynamic=rel_x,rel_y,rel_z,heading_cos,heading_sin per waypoint # - **Partners** (MAX_PARTNERS x 9): rel_x, rel_y, rel_z, length, width, heading_cos, heading_sin, sim_speed_signed, seconds_stopped -# - **Lanes** (MAX_LANES x 7): rel_x, rel_y, rel_z, seg_length, seg_width, dir_cos, dir_sin -# - **Boundaries** (MAX_BOUNDS x 7): same as lanes +# - **Lanes** (MAX_LANES x 9): rel_x, rel_y, rel_z, seg_length, dir_cos, dir_sin, seg_width, goal_dist_abs, goal_dist_rel +# - **Boundaries** (MAX_BOUNDS x 6): first 6 lane features # - **Traffic controls** (MAX_TRAFFIC x 7): rel_x1, rel_y1, rel_x2, rel_y2, rel_z, type, state # %% @@ -598,7 +598,7 @@ def unpack_all_timesteps(bufs, agent_idx): for i in range(lanes.shape[0]): if np.allclose(lanes[i], 0): continue - rx, ry, rz, length, _, dc, ds = lanes[i][:7] + rx, ry, rz, length, dc, ds = lanes[i][:6] ax.plot( [rx - dc * length / 2, rx + dc * length / 2], [ry - ds * length / 2, ry + ds * length / 2], @@ -620,7 +620,7 @@ def unpack_all_timesteps(bufs, agent_idx): for i in range(boundaries.shape[0]): if np.allclose(boundaries[i], 0): continue - rx, ry, rz, length, _, dc, ds = boundaries[i][:7] + rx, ry, rz, length, dc, ds = boundaries[i][:6] ax.plot( [rx - dc * length / 2, rx + dc * length / 2], [ry - ds * length / 2, ry + ds * length / 2], @@ -865,7 +865,7 @@ def unpack_all_timesteps(bufs, agent_idx): # %% # Road per-feature distributions (lanes + boundaries) -road_labels = ["rel_x", "rel_y", "rel_z", "seg_length", "seg_width", "dir_cos", "dir_sin"] +road_labels = ["rel_x", "rel_y", "rel_z", "seg_length", "dir_cos", "dir_sin"] lf = env.lane_features bf = env.boundary_features max_lanes = env.obs_slots_lane_kept @@ -891,7 +891,7 @@ def unpack_all_timesteps(bufs, agent_idx): f"({100 * len(vis_bounds) / (all_bounds.shape[0] * max_bounds):.1f}%)" ) -fig, axes = plt.subplots(2, 7, figsize=(28, 8)) +fig, axes = plt.subplots(2, 6, figsize=(24, 8)) for i, label in enumerate(road_labels): # Lanes axes[0, i].hist(vis_lanes[:, i], bins=80, edgecolor="black", alpha=0.7, color="silver") diff --git a/pufferlib/ocean/drive/constants.h b/pufferlib/ocean/drive/constants.h index 05e038263d..1160a52a80 100644 --- a/pufferlib/ocean/drive/constants.h +++ b/pufferlib/ocean/drive/constants.h @@ -219,7 +219,7 @@ static const int ROAD_OFFSETS[25][2] #define EGO_FEATURES 10 #define LANE_FEATURES 9 -#define BOUNDARY_FEATURES 9 +#define BOUNDARY_FEATURES 6 #define PARTNER_FEATURES 9 #define TRAFFIC_CONTROL_FEATURES 7 #define GOAL_FEATURES 3 diff --git a/pufferlib/ocean/drive/drive.h b/pufferlib/ocean/drive/drive.h index 2c39bc8049..50eec3d0d5 100644 --- a/pufferlib/ocean/drive/drive.h +++ b/pufferlib/ocean/drive/drive.h @@ -3807,8 +3807,11 @@ static int write_reward_target_obs(Drive *env, Agent *ego, float *obs, int obs_i ego->list_goal_y[goal_idx], &rel_goal_x, &rel_goal_y); - obs[obs_idx++] = rel_goal_x / env->obs_norm_goal_offset_m; - obs[obs_idx++] = rel_goal_y / env->obs_norm_goal_offset_m; + // Goals beyond obs_norm_goal_offset_m collapse to a unit direction vector (keeps obs in [-1, 1]) + float goal_distance_m = sqrtf(rel_goal_x * rel_goal_x + rel_goal_y * rel_goal_y); + float goal_normalization_m = fmaxf(env->obs_norm_goal_offset_m, goal_distance_m); + obs[obs_idx++] = rel_goal_x / goal_normalization_m; + obs[obs_idx++] = rel_goal_y / goal_normalization_m; obs[obs_idx++] = (ego->list_goal_z[goal_idx] - ego->sim_z) / env->obs_norm_z_m; } @@ -4020,11 +4023,12 @@ static int write_road_obs(Drive *env, Agent *ego, float *obs, int obs_idx, int * segment_dest[feature_base + 1] = rel_y / env->obs_norm_xy_offset_m; segment_dest[feature_base + 2] = rel_z / env->obs_norm_z_m; segment_dest[feature_base + 3] = seg_half_len / env->obs_norm_road_seg_length_m; - segment_dest[feature_base + 4] = LANE_WIDTH / env->obs_norm_road_seg_width_m; - segment_dest[feature_base + 5] = rel_seg_dir_x; - segment_dest[feature_base + 6] = rel_seg_dir_y; + segment_dest[feature_base + 4] = rel_seg_dir_x; + segment_dest[feature_base + 5] = rel_seg_dir_y; // Goal-distance features: absolute and relative to ego's lane->goal distance. if (is_lane) { + // Constant until the map format carries per-lane width + segment_dest[feature_base + 6] = LANE_WIDTH / env->obs_norm_road_seg_width_m; float goal_dist_abs = 0.0f, goal_dist_rel = 0.0f; // 0 when flag off / unresolved if (env->obs_goal_lane_distance && goal_graph_idx >= 0 && entity_idx < env->num_road_elements) { int lane_graph_idx = env->lane_graph.lane_to_graph_idx[entity_idx]; @@ -4039,10 +4043,6 @@ static int write_road_obs(Drive *env, Agent *ego, float *obs, int obs_idx, int * } segment_dest[feature_base + 7] = goal_dist_abs; segment_dest[feature_base + 8] = goal_dist_rel; - } else { - // NOTE: Remove this with next model - segment_dest[feature_base + 7] = 0.0f; - segment_dest[feature_base + 8] = 0.0f; } } diff --git a/pufferlib/ocean/drive/drive.py b/pufferlib/ocean/drive/drive.py index 1ded1791af..c34e1d9b15 100644 --- a/pufferlib/ocean/drive/drive.py +++ b/pufferlib/ocean/drive/drive.py @@ -6,6 +6,8 @@ import pufferlib from pufferlib.ocean.drive import binding +TRAFFIC_CONTROL_CATEGORICAL_FEATURE_COUNT = 2 # type and state + def compute_effective_road_obs_count(max_count, dropout): if max_count <= 0: @@ -271,6 +273,17 @@ def __init__( self.single_observation_space = gymnasium.spaces.Box(low=-1, high=1, shape=(self.num_obs,), dtype=np.float32) + # Observation distribution stats exclude raw traffic-control categories and valid-slot counts. + self.obs_stats_feature_mask = np.ones(self.num_obs, dtype=bool) + valid_counts_start_idx = self.num_obs - self.obs_valid_count_features + traffic_controls_start_idx = ( + valid_counts_start_idx - self.obs_slots_traffic_controls_n * self.traffic_control_features + ) + for slot_idx in range(self.obs_slots_traffic_controls_n): + slot_end_idx = traffic_controls_start_idx + (slot_idx + 1) * self.traffic_control_features + self.obs_stats_feature_mask[slot_end_idx - TRAFFIC_CONTROL_CATEGORICAL_FEATURE_COUNT : slot_end_idx] = False + self.obs_stats_feature_mask[valid_counts_start_idx:] = False + self.init_step = init_step # Per C environment randomized start point. When on, each parallel environment # starts the episode at a randomized point. diff --git a/pufferlib/pufferl.py b/pufferlib/pufferl.py index c5809b4adb..a1ed9a6bc6 100644 --- a/pufferlib/pufferl.py +++ b/pufferlib/pufferl.py @@ -301,6 +301,9 @@ def __init__(self, config, vecenv, policy, logger=None): # Initializations self.config = config self.vecenv = vecenv + self.obs_stats_feature_idx = torch.as_tensor( + np.flatnonzero(vecenv.driver_env.obs_stats_feature_mask), device=config["device"] + ) self.epoch = 0 self.global_step = 0 self.agent_steps = 0 @@ -393,9 +396,12 @@ def evaluate(self): # Obs distribution stats (max/min/mean across the batch and obs # dims, appended per env step). Surfaces clipping / unbounded # features / normalization regressions in wandb. - self.stats["obs/max"].append(o_device.max().item()) - self.stats["obs/min"].append(o_device.min().item()) - self.stats["obs/mean"].append(o_device.mean().item()) + obs_stat_source = ( + o_device if self.obs_stats_feature_idx is None else o_device[..., self.obs_stats_feature_idx] + ) + self.stats["obs/max"].append(obs_stat_source.max().item()) + self.stats["obs/min"].append(obs_stat_source.min().item()) + self.stats["obs/mean"].append(obs_stat_source.mean().item()) profile("eval_forward", epoch) with torch.no_grad(), self.amp_context: diff --git a/pufferlib/viz.py b/pufferlib/viz.py index e70411d504..e6f547f86b 100644 --- a/pufferlib/viz.py +++ b/pufferlib/viz.py @@ -692,7 +692,7 @@ def plot_observation( count_lane += 1 rel_x, rel_y = lane_obs[i][0], lane_obs[i][1] length = lane_obs[i][3] * rl2p - dir_cos, dir_sin = lane_obs[i][5], lane_obs[i][6] + dir_cos, dir_sin = lane_obs[i][4], lane_obs[i][5] # idx 7 = goal_dist_abs (0 near goal lane -> 1 far/unreachable); green->red colormap color = plt.cm.RdYlGn_r(float(lane_obs[i][7])) if obs_goal_lane_distance else "lightgrey" ax.scatter(rel_x, rel_y, color=color, s=10, zorder=1) @@ -711,7 +711,7 @@ def plot_observation( count_boundary += 1 rel_x, rel_y = boundary_obs[i][0], boundary_obs[i][1] length = boundary_obs[i][3] * rl2p - dir_cos, dir_sin = boundary_obs[i][5], boundary_obs[i][6] + dir_cos, dir_sin = boundary_obs[i][4], boundary_obs[i][5] color = "black" ax.scatter(rel_x, rel_y, color=color, s=10, zorder=1) ax.plot( @@ -1453,7 +1453,7 @@ def _render_interactive_replay_payload(compressed_payload, filename): const trafficStart = p; const rot = (x,y) => [-y,x]; const zero = (off,n) => { for(let i=0;i { const out=[]; for(let i=0;i { const out=[]; for(let i=0;i