Ev/coarse lane view - WIP DO NOT REVIEW#464
Conversation
Adds a per-agent top-K coarse-view observation built from samples spaced along all drivable lanes, augmented with Dijkstra distance to the agent's current goal (absolute + min-anchored relative). Replaces close-range lane centerline obs slots in the paper-aligned configuration. For each agent each step, emit obs_slots_coarse_n slots × 6 floats: ego-frame position (2), ego-relative heading (2), abs and min-anchored relative Dijkstra distance to the current goal (2). The lane graph is directed, so unreachable sample→goal pairs fall back to the closest spatially-reachable coarse sample (euclidean leg + graph leg) — this encodes a U-turn / lane-change cost without requiring undirected graph preprocessing. Off by default (obs_slots_coarse_n = 0). Map-load: build per-lane cumulative arclength on RoadMapElement, the road_to_lane_graph reverse map, and the coarse_samples array. All live on SharedMapData so use_map_cache shares them across envs. Goal projection (goal_lane_graph_idx + goal_along_s) refreshes at every goal-set / goal-advance site (compute_goals success path, set_start_position replay branch, c_step goal-advance block). Removed from reset_agent_state since goal projection is owned by the goal pipeline and outlives the generic per-episode state reset. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Lane slots now carry coarse-view samples directly: the close-range lane-segment branch of write_road_obs is replaced with a top-K nearest coarse-sample scan (40m-spaced points along all drivable lanes within obs_range_coarse_m), and the existing 7-wide ROAD_FEATURES layout is reinterpreted in-place as [rel_x, rel_y, rel_z, dist_abs, dist_rel_min, cos_dh, sin_dh]. Boundary slots are untouched and still carry close-range ROAD_EDGE polyline segments via the grid map. Side effects of folding coarse into lane: obs_slots_coarse_n and COARSE_FEATURES disappear (lane slot count + ROAD_FEATURES already do that job), and the policy's existing lane_encoder consumes the new content with zero changes — same slot width, same plumbing. viz.py plot_observation: lane drawing reinterprets the 7 floats as coarse content, scatters dots colored by min-anchored dist_rel via RdYlGn_r (routing-best slot gets a lime ring), draws a short heading tick per dot. Padding check switched to the PADDED_OBSERVATION_VALUE sentinel. Fixes a pre-existing _img_from_fig DPI-mismatch crash on HiDPI displays by switching to buffer_rgba. scripts/render_coarse_obs.py: steps a Drive env and dumps the plot_observation render at configurable intervals so we can inspect how the coarse view evolves with the agent's position. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the obs_html eval at interval=250 (~ once per 1B-step run) with the egl mp4 backend at interval=20 (~ every 50M env steps), capturing both bev (top-down following the agent) and sim_state (chase) views per scenario. render_num_scenarios drops to 4 so each eval cycle stays cheap. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… args
submit_cluster.py serializes the YAML list ["bev","sim_state"] as a Python
list repr ('[\"bev\",\"sim_state\"]') and passes it to argparse as a single
shell token, which trips the inner shell. The two views we want are already
the drive.ini default for validation_gigaflow.render_views, so the override
was redundant anyway.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
pufferl's argparser only generates --eval.<name>.<key> CLI flags for keys already present in the [eval.<name>] ini section. interval is set in [eval.validation_defaults] but NOT in [eval.validation_gigaflow], so --eval.validation-gigaflow.interval was an unrecognized arg. Override on the parent defaults section instead; validation_gigaflow inherits and the other inheriting evals are all disabled in this yaml. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Before this, --eval.<name>.<key> CLI flags only existed for keys physically present in the [eval.<name>] ini section. Overriding a key that only lived in the parent (inherits = "validation_defaults") meant either adding placeholder lines to the child section or routing the override through the parent (which then affects every sibling that inherits the same parent). Either way pollutes config. Add a second pass that walks the `inherits` chain of each [eval.<name>] section and registers --eval.<name>.<inherited_key> flags backed by argparse.SUPPRESS, so the value is only carried into the nested config dict when the user actually passes the flag. Inheritance merge in EvalManager continues to use the parent value otherwise. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
_resolve_inherits_chain returns [] for validation_defaults (no `inherits` key), so the inner loop already does nothing for it. The explicit skip was misleading — it suggested some special semantic for that section when there isn't one. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR is marked "WIP DO NOT REVIEW". It replaces the lane channel of the drive env observation with a GIGAFLOW "W_lane" coarse view: every drivable lane is sampled every 40 m at map load, and per step the top-K nearest samples within obs_range_coarse_m are emitted with absolute + min-anchored Dijkstra distances to the agent's projected goal. Boundary slots remain ROAD_EDGE polylines. Supporting changes wire new config knobs through the Python env, viz, an inherits-aware CLI override path for pufferl, and a debug rendering script.
Changes:
- New C-side coarse-view machinery: per-lane arclengths,
CoarseSamplearray, road↔lane-graph map, and a rewrittenwrite_road_obslane branch using top-K + lane-graph distances. - Plumbed
coarse_sample_spacing_m,obs_range_coarse_m,obs_norm_coarse_dist_mthroughdrive.py/binding.c/drive.ini, plus aneval.<name>.<inherited_key>override path inpufferl.py. - Viz updates:
_img_from_figswitched tobuffer_rgba;plot_observationlane branch redrawn for coarse samples; addedscripts/render_coarse_obs.pydriver and updated single-agent eval YAML.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| pufferlib/ocean/drive/drive.h | Core coarse-view build + per-step top-K and Dijkstra distance emission |
| pufferlib/ocean/drive/datatypes.h | CoarseSample struct, cumulative_s on RoadMapElement, goal projection fields on Agent |
| pufferlib/ocean/drive/drive.py | Surfaces new coarse-view kwargs to the env constructor and kwargs dict |
| pufferlib/ocean/drive/binding.c | Unpacks new coarse-view kwargs into the C struct |
| pufferlib/config/ocean/drive.ini | Adds coarse-view INI keys and comments |
| pufferlib/viz.py | RGBA capture fix; redrew lane slot rendering for coarse samples |
| pufferlib/pufferl.py | Adds CLI override flags for inherited eval section keys |
| scripts/render_coarse_obs.py | New helper to dump per-frame coarse-view obs renders |
| scripts/cluster_configs/single_agent_speed_run.yaml | Switches validation_gigaflow to egl renders with new interval override |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| char reachable[env->n_coarse_samples]; | ||
| for (int s = 0; s < env->n_coarse_samples; s++) { | ||
| int slg = env->coarse_samples[s].lane_graph_idx; | ||
| if (slg < 0 || goal_lg < 0) { | ||
| reachable[s] = 0; | ||
| continue; | ||
| } | ||
| if (slg == goal_lg) { | ||
| reachable[s] = 1; | ||
| continue; | ||
| } | ||
| float gd = env->lane_graph.distances[slg * n_lanes + goal_lg]; | ||
| reachable[s] = (isfinite(gd) && gd < COARSE_DIST_MAX) ? 1 : 0; | ||
| } |
| // Per-sample reachability to goal lane via the directed lane graph. | ||
| int n_lanes = env->lane_graph.n_lanes; | ||
| int goal_lg = ego->goal_lane_graph_idx; | ||
| char reachable[env->n_coarse_samples]; | ||
| for (int s = 0; s < env->n_coarse_samples; s++) { | ||
| int slg = env->coarse_samples[s].lane_graph_idx; | ||
| if (slg < 0 || goal_lg < 0) { | ||
| reachable[s] = 0; | ||
| continue; | ||
| } | ||
| if (slg == goal_lg) { | ||
| reachable[s] = 1; | ||
| continue; | ||
| } | ||
| float gd = env->lane_graph.distances[slg * n_lanes + goal_lg]; | ||
| reachable[s] = (isfinite(gd) && gd < COARSE_DIST_MAX) ? 1 : 0; | ||
| } | ||
|
|
||
| float abs_dist[K]; | ||
| float min_abs = FLT_MAX; | ||
| for (int k = 0; k < n_sel; k++) { | ||
| struct CoarseSample *cs = &env->coarse_samples[sel_idx[k]]; | ||
| if (reachable[sel_idx[k]]) { | ||
| abs_dist[k] | ||
| = coarse_lane_dist(env, cs->lane_graph_idx, cs->along_s, goal_lg, ego->goal_along_s); | ||
| } else { | ||
| float best_d2 = FLT_MAX; | ||
| int best_idx = -1; | ||
| for (int s = 0; s < env->n_coarse_samples; s++) { | ||
| if (!reachable[s]) { | ||
| continue; | ||
| } | ||
| float dx = env->coarse_samples[s].x - cs->x; | ||
| float dy = env->coarse_samples[s].y - cs->y; | ||
| float d2 = dx * dx + dy * dy; | ||
| if (d2 < best_d2) { | ||
| best_d2 = d2; | ||
| best_idx = s; | ||
| } | ||
| } | ||
| if (best_idx >= 0) { | ||
| struct CoarseSample *cr = &env->coarse_samples[best_idx]; | ||
| float spatial_leg = sqrtf(best_d2); | ||
| float graph_leg | ||
| = coarse_lane_dist(env, cr->lane_graph_idx, cr->along_s, goal_lg, ego->goal_along_s); | ||
| abs_dist[k] = spatial_leg + graph_leg; | ||
| if (abs_dist[k] > COARSE_DIST_MAX) { | ||
| abs_dist[k] = COARSE_DIST_MAX; | ||
| } | ||
| } else { | ||
| abs_dist[k] = COARSE_DIST_MAX; | ||
| } | ||
| } |
| coarse_sample_spacing_m = 40.0 | ||
| obs_range_coarse_m = 200.0 | ||
| obs_norm_coarse_dist_m = 200.0 |
| count_boundary = 0 | ||
| for i in range(boundary_obs.shape[0]): | ||
| if np.all(boundary_obs[i] == 0): | ||
| if np.all(np.isclose(boundary_obs[i], -0.001)) or np.all(boundary_obs[i] == 0): |
| static void find_nearest_drivable_lane(Drive *env, float x, float y, int *out_road_idx, float *out_along_s) { | ||
| int best_road = -1; | ||
| float best_along_s = 0.0f; | ||
| float best_d2 = FLT_MAX; | ||
| for (int i = 0; i < env->num_road_elements; i++) { | ||
| RoadMapElement *r = &env->road_elements[i]; | ||
| if (r->cumulative_s == NULL || env->road_to_lane_graph[i] < 0) { | ||
| continue; | ||
| } | ||
| for (int seg = 0; seg < r->segment_length - 1; seg++) { | ||
| float dx = r->x[seg + 1] - r->x[seg]; | ||
| float dy = r->y[seg + 1] - r->y[seg]; | ||
| float len2 = dx * dx + dy * dy; | ||
| float t = (len2 > 0.0f) ? ((x - r->x[seg]) * dx + (y - r->y[seg]) * dy) / len2 : 0.0f; | ||
| if (t < 0.0f) t = 0.0f; | ||
| if (t > 1.0f) t = 1.0f; | ||
| float px = r->x[seg] + t * dx; | ||
| float py = r->y[seg] + t * dy; | ||
| float d2 = (px - x) * (px - x) + (py - y) * (py - y); | ||
| if (d2 < best_d2) { | ||
| best_d2 = d2; | ||
| best_road = i; | ||
| float seg_s0 = r->cumulative_s[seg]; | ||
| float seg_s1 = r->cumulative_s[seg + 1]; | ||
| best_along_s = seg_s0 + t * (seg_s1 - seg_s0); | ||
| } | ||
| } | ||
| } | ||
| *out_road_idx = best_road; | ||
| *out_along_s = best_along_s; | ||
| } |
| // GIGAFLOW W_lane derived data (depends on road_elements + lane_graph being loaded). | ||
| build_lane_arclengths(env); | ||
| build_road_to_lane_graph(env); | ||
| build_coarse_samples(env); |
No description provided.