diff --git a/pufferlib/config/ocean/drive.ini b/pufferlib/config/ocean/drive.ini deleted file mode 100644 index 5283573d38..0000000000 --- a/pufferlib/config/ocean/drive.ini +++ /dev/null @@ -1,623 +0,0 @@ -[base] -package = ocean -env_name = puffer_drive -policy_name = Drive -; Options: Recurrent, None -rnn_name = None - -[vec] -num_envs = 20 -num_workers = auto -batch_size = auto - -[policy] -; Encoder layer (per-encoder embedding width) -ego_input_size = 64 -partner_input_size = 64 -lane_input_size = 64 -boundary_input_size = 64 -traffic_control_input_size = 64 -context_input_size = 64 -; Encoder activation - options: "relu", "tanh", "gelu" -encoder_activation = "relu" -encoder_layer_norm = True -mask_padded_features = False -; Shared backbone layer -backbone_hidden_size = 512 -backbone_num_layers = 4 -; Backbone activation - options: "relu", "tanh", "gelu" -backbone_activation = "gelu" -backbone_layer_norm = False -; Actor head layer -actor_hidden_size = 512 -actor_num_layers = 0 -; Critic head layer -critic_hidden_size = 512 -critic_num_layers = 0 -; Dual or shared actor-critic backbone -shared_network = True - -[rnn] -input_size = 512 -hidden_size = 512 - -[env] -; --- Simulation --- -; Options: "gigaflow", "replay" -simulation_mode = "gigaflow" -; Total number of agents in the environment -num_agents = 1024 -; GIGAFLOW-specific: minimum/maximum number of agents per environment -min_agents_per_env = 1 -max_agents_per_env = 80 -; Actions type - options: "discrete", "continuous" -action_type = "discrete" -; Dynamics model - options: "classic", "jerk" -dynamics_model = "jerk" -; Time delta between steps in seconds -dt = 0.1 -; Optional nonzero launch speed for gigaflow random spawns -spawn_initial_speed = 0.0 -; Collision behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove -collision_behavior = 1 -; Offroad behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove -offroad_behavior = 1 -; Traffic light behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove -traffic_light_behavior = 1 -; Share static map geometry (roads/grid/lane-graph) across envs using the same map - 0 off, 1 on -use_map_cache = 0 -; Number of steps before reset -scenario_length = 1280 -; Frequency of resampling scenario (in steps), 0 to disable -resample_frequency = 12_800_0000 -; End of episode - options: 0 - scenario_length, 1 - early reset when too many agents are removed -termination_mode = 1 -; Ratio of inactive agents (stopped/removed) that triggers early reset (used when termination_mode = 1) -inactive_agent_threshold = 0.4 -; Determines which step of the trajectory to initialize the agents at upon reset -init_step = 0 -; options: "control_vehicles", "control_agents", "control_tracks_to_predict", "control_sdc_only" -control_mode = "control_vehicles" -; Controller used by agent 0, the canonical SDC/target. -; options: "static", "policy", "replay", "idm" -sdc_controller = "policy" -; Controller used by non-SDC vehicles. -; options: "static", "policy", "replay", "idm" -non_sdc_controller = "policy" -; Controller used by non-vehicle agents. "auto" follows non_sdc_controller unless it is "idm", then it uses "replay". -; options: "auto", "static", "policy", "replay", "idm" -non_vehicle_controller = "auto" -; Options: "created_all_valid", "create_only_controlled" -init_mode = "create_all_valid" -; Enable computation of evaluation-only metrics -compute_eval_metrics = False - -; --- Goal / Target --- -; Target representation - options: "static", "dynamic" -target_type = "static" -; True: place goals along the agent's route (existing behavior, on-lane and -; in front of the agent). False: scatter each goal at a uniformly random -; drivable point anywhere on the map. -goal_on_lane = True -; Meters around goal to be considered "reached" -goal_radius = 2.0 -; Maximum speed at final waypoint to count goal reward -goal_speed = 1000.0 -; Number of target waypoints in the observation (including goal) -num_target_waypoints = 3 -; Waypoint spacing range in meters when building path -min_waypoint_spacing = 20.0 -max_waypoint_spacing = 60.0 - -; --- Rewards --- -reward_conditioning = False -reward_randomization = False -reward_goal = 1.0 -reward_collision = 1.5 -reward_offroad = 1.5 -reward_stop_line = 1.0 -reward_comfort = 0.05 -reward_lane_align = 0.025 -reward_vel_align = 1.0 -reward_lane_center = 0.0038 -reward_center_bias = 0.0 -reward_velocity = 0.0025 -reward_reverse = 0.005 -reward_timestep = 0.000025 -reward_overspeed = 0.05 -reward_ade = 0.0 - -; --- Map --- -; Path to map used for training -map_dir = "pufferlib/resources/drive/binaries/carla" -; Number of maps to load from map_dir -num_maps = 8 - -; --- Observation slot counts --- -obs_slots_lane_n = 80 -obs_slots_boundary_n = 80 -obs_slots_partners_n = 16 -obs_slots_traffic_controls_n = 4 -; Fraction of segment observation slots to drop (reduces obs size) -obs_dropout_lane = 0.0 -obs_dropout_boundary = 0.0 -; Stride for lane and boundary segments; 1 means use every segment, 2 means use every other segment, etc. -obs_lane_stride = 1 -obs_boundary_stride = 1 -; --- Observation normalization --- -obs_norm_goal_offset_m = 120.0 -obs_norm_xy_offset_m = 120.0 -obs_norm_veh_length_m = 15.0 -obs_norm_veh_width_m = 10.0 -obs_norm_road_seg_length_m = 10.0 -obs_norm_road_seg_width_m = 5.0 -; --- Observation ranges (meters, ego frame) --- -obs_range_road_front_m = 120.0 -obs_range_road_behind_m = 20.0 -obs_range_road_side_m = 30.0 -obs_range_partner_m = 100.0 -obs_range_traffic_control_m = 100.0 - -; --- Robustness features--- -; Erratic agents are selected once per episode; masked out of the PPO rollout buffer. -; Per-episode probability that an agent is blind to other agents for the whole episode -partner_blindness_prob = 0.0 -; Per-step probability that a flagged blind-partner agent zeros out its partner observations -partner_blindness_trigger_prob = 0.0 -; Per-episode probability that an agent is flagged as a phantom braker for the whole episode -phantom_braking_prob = 0.0 -; Per-step probability that a flagged phantom-braker actually initiates a brake window -phantom_braking_trigger_prob = 0.0 -; Duration (in steps) of phantom braking once triggered -phantom_braking_duration = 10 - -[train] -total_timesteps = 10_000_000_000 -checkpoint_interval = 50 -anneal_lr = True -; batch_size = num_workers * num_agents * bptt_horizon -batch_size = auto -minibatch_size = 65_536 -max_minibatch_size = 65_536 -bptt_horizon = 128 -learning_rate = 0.0005 -gae_lambda = 0.95 -gamma = 0.999 -ent_coef = 0.01 -clip_coef = 0.2 -max_grad_norm = 0.5 -vf_clip_coef = None -vf_coef = 0.5 -update_epochs = 2 -normalize_rewards = True -; --- -adam_beta1 = 0.9 -adam_beta2 = 0.999 -adam_eps = 1e-8 -vtrace_c_clip = 1 -vtrace_rho_clip = 1 -adv_sampling_prio_alpha = 0.8499999999999999 -adv_sampling_prio_beta0 = 0.8499999999999999 -adv_filter_ewma_beta = 0.25 -adv_filter_threshold_scale = 0.01 -; Rendering options -render = False -render_interval = 1000 -; If True, show exactly what the agent sees in agent observation -obs_only = True -; Show grid lines -show_grid = False -; Draws lines from ego agent observed ORUs and road elements to show detection range -show_lasers = False -; Display human xy logs in the background -show_human_logs = True -; Options: List[str to path], str to path (e.g., "resources/drive/training/binaries/map_001.bin"), None -render_map = none - -; =========================================================================== -; Evaluation suites -; -; Each [eval.] section is one Evaluator instance. EvalManager discovers -; them via auto-discovery (any section under [eval] with a `type` field). -; Sections without a `type` field are templates — referenced from other -; sections via `inherits = ""`. -; -; Field reference: -; type — registered evaluator class (multi_scenario, behavior_class, -; human_replay, wosac) -; enabled — true|false -; interval — epochs between runs (0 disables) -; mode — "inline" (block training) | "subprocess" (spawn process) -; inherits — pull defaults from another section, recursively -; clean — true → zero perturbations + dropout + enforce red lights -; render — true → capture mp4(s) during rollout -; render_views — list of camera views: sim_state, bev, topdown_sim, bev_all -; env. — any [env] override (dotted key) -; eval. — evaluator-specific knob (e.g. num_scenarios) -; vec. — any [vec] override -; =========================================================================== - -[mine] -; Failure-mining workflow. `puffer mine_failures puffer_drive ...` -; rolls a trained policy out N times and writes compact-replay HTML -; for episodes whose episode_return is strictly below score_threshold. -output_dir = "" -num_episodes = 100 -score_threshold = -inf -render = true - -; --------------------------------------------------------------------------- -; Multi-scenario validation eval. validation_defaults holds the shared -; clean-eval env + fixed eval reward weights; the replay / gigaflow sections -; add their sim-specific map set and agent layout. Each sweeps 250 scenarios, -; ignores traffic lights, and disables reward randomization so metrics are -; comparable across checkpoints. Both write a per-episode CSV and a coverage -; report (episode_metrics/). -; --------------------------------------------------------------------------- - -[eval.validation_defaults] -enabled = true -interval = 250 -mode = "inline" -clean = true -env.eval_mode = 1 -env.collision_behavior = 1 -env.offroad_behavior = 1 -; Explicit 0 wins over the clean macro's red-light enforcement. -env.traffic_light_behavior = 0 -env.reward_randomization = False -env.termination_mode = 0 -env.num_agents = 1024 -env.target_type = "static" -env.goal_speed = 3.0 -env.reward_collision = 3.0 -env.reward_offroad = 3.0 -env.reward_stop_line = 1.0 -env.reward_goal = 1.0 -env.reward_overspeed = 0.05 -env.reward_comfort = 0.05 -env.reward_velocity = 0.0025 -env.reward_lane_align = 0.025 -env.reward_lane_center = 0.0038 -env.reward_timestep = 0.000025 -env.reward_reverse = 0.005 -env.reward_ade = 0.0 -env.obs_dropout_lane = 0.0 -env.obs_dropout_boundary = 0.0 -env.obs_slots_lane_n = 80 -env.obs_slots_boundary_n = 80 -eval.num_scenarios = 250 -eval.export_episode_csv = true -eval.verify_coverage = true - -[eval.validation_replay] -inherits = "validation_defaults" -type = "multi_scenario" -enabled = true -render = true -render_backend = "triage_html" -env.simulation_mode = "replay" -env.control_mode = "control_sdc_only" -env.map_dir = "/scratch/ev2237/data/nuplan/nuplan_mini_train_bins" -env.num_maps = 250 -env.max_agents_per_env = 64 -env.scenario_length = 200 -env.resample_frequency = 200 -eval.render_num_scenarios = 5 -eval.render_max_steps = 200 - -[eval.validation_gigaflow] -inherits = "validation_defaults" -type = "multi_scenario" -enabled = true -render = true -render_backend = "egl" -render_views = ["sim_state", "bev"] -env.simulation_mode = "gigaflow" -env.map_dir = "pufferlib/resources/drive/binaries/carla" -env.num_maps = 8 -env.num_agents = 1024 -env.min_agents_per_env = 40 -env.max_agents_per_env = 40 -env.scenario_length = 500 -env.resample_frequency = 500 -eval.render_num_scenarios = 8 -eval.render_max_steps = 300 - -[eval.dnf_triage] -inherits = "validation_defaults" -type = "multi_scenario" -enabled = false -render = true -render_backend = "triage_html" -env.simulation_mode = "gigaflow" -env.map_dir = "pufferlib/resources/drive/binaries/carla/opendrive__Town10HD.bin" -env.num_maps = 1 -env.min_agents_per_env = 1 -env.max_agents_per_env = 1 -env.scenario_length = 500 -env.resample_frequency = 500 -eval.num_scenarios = 32 -eval.render_num_scenarios = 16 -eval.render_max_steps = 300 - -; --------------------------------------------------------------------------- -; Driving-behaviour evaluation: nuPlan scenes labeled by scene type. Each -; behavior is one [eval.behaviors_*] section. All inherit from the template -; below — change shared knobs in one place. -; --------------------------------------------------------------------------- - -[eval.behaviors_defaults] -enabled = false -interval = 250 -mode = "inline" -clean = true -render = true -render_views = ["sim_state", "bev"] -env.simulation_mode = "replay" -env.control_mode = "control_sdc_only" -env.init_mode = "create_all_valid" -env.scenario_length = 201 -env.obs_slots_partners_n = 32 -eval.num_scenarios = 50 -eval.render_num_scenarios = 2 -eval.render_max_steps = 200 - -[eval.behaviors_full_dir] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/nuplan_mini_train_bins" - -[eval.behaviors_hard_stop] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/hard_stop" - -[eval.behaviors_highway_straight] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/highway_straight" - -[eval.behaviors_lane_change] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/lane_change" - -[eval.behaviors_merge] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/merge" - -[eval.behaviors_parked_cars] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/parked_cars" - -[eval.behaviors_roundabout] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/roundabout" - -[eval.behaviors_stopped_traffic] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/stopped_traffic" - -[eval.behaviors_traffic_light_green] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/traffic_light_green" - -[eval.behaviors_traffic_light_stop] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/traffic_light_stop" - -[eval.behaviors_unprotected_left] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/unprotected_left" - -[eval.behaviors_unprotected_right] -inherits = "behaviors_defaults" -type = "behavior_class" -enabled = true -env.map_dir = "/scratch/ev2237/data/nuplan/categories_v021/unprotected_right" - -; --------------------------------------------------------------------------- -; Optional: WOSAC realism eval. Off by default. -; --------------------------------------------------------------------------- - -[eval.wosac] -type = "wosac" -enabled = false -interval = 500 -mode = "subprocess" -clean = true -render = false -env.control_mode = "control_wosac" -env.init_mode = "create_all_valid" -env.init_step = 10 -env.goal_radius = 2.0 -eval.wosac_num_rollouts = 32 -eval.wosac_num_agents = 256 -eval.wosac_sanity_check = false -eval.wosac_aggregate_results = true - -; [sweep.train.learning_rate] -; distribution = log_normal -; min = 0.0001 -; mean = 0.001 -; max = 0.002 -; scale = auto - -; [sweep.train.ent_coef] -; distribution = log_normal -; min = 0.0005 -; mean = 0.01 -; max = 0.02 -; scale = auto - -; [sweep.train.vf_clip_coef] -; distribution = log_normal -; min = 0.1 -; mean = 0.2 -; max = 3 -; scale = auto - -; [sweep.env.inactive_agent_threshold] -; distribution = log_normal -; min = 0.1 -; mean = 0.4 -; max = 1.0 -; scale = auto - -; [sweep.train.gamma] -; distribution = logit_normal -; min = 0.90 -; mean = 0.99 -; max = 0.9999 -; scale = auto - -; [sweep.train.max_grad_norm] -; distribution = uniform -; min = 0.2 -; mean = 0.5 -; max = 2.0 -; scale = auto - -; [sweep.train.update_epochs] -; distribution = int_uniform -; min = 1 -; mean = 1 -; max = 6 -; scale = auto - -; [sweep.train.vf_coef] -; distribution = uniform -; min = 0.2 -; mean = 0.5 -; max = 3.0 -; scale = auto - -; [sweep.train.vtrace_rho_clip] -; distribution = uniform -; min = 0.0 -; max = 5.0 -; mean = 1.0 -; scale = auto - -; [sweep.train.vtrace_c_clip] -; distribution = uniform -; min = 0.0 -; max = 5.0 -; mean = 1.0 -; scale = auto - -; [sweep.train.clip_coef] -; distribution = uniform -; min = 0.01 -; max = 1.0 -; mean = 0.2 -; scale = auto - -; [sweep.train.gae_lambda] -; distribution = logit_normal -; min = 0.8 -; mean = 0.95 -; max = 0.995 -; scale = auto - -; [sweep.policy.actor_num_layers] -; distribution = int_uniform -; min = 1 -; mean = 1 -; max = 2 -; scale = auto - -; [sweep.policy.critic_num_layers] -; distribution = int_uniform -; min = 1 -; mean = 1 -; max = 2 -; scale = auto - -; [sweep.env.reward_goal] -; distribution = uniform -; min = 0.0 -; mean = 0.5 -; max = 1.0 -; scale = auto - -; [sweep.env.reward_comfort] -; distribution = uniform -; min = 0.0 -; mean = 0.05 -; max = 0.2 -; scale = auto - -; [sweep.env.reward_lane_align] -; distribution = uniform -; min = 0.0 -; mean = 0.025 -; max = 0.2 -; scale = auto - -; [sweep.env.reward_lane_center] -; distribution = uniform -; min = 0.0 -; mean = 0.0038 -; max = 0.2 -; scale = auto - -; [sweep.env.reward_velocity] -; distribution = uniform -; min = 0.0 -; mean = 0.0025 -; max = 0.01 -; scale = auto - -; [sweep.train.adam_beta1] -; distribution = logit_normal -; min = 0.7 -; mean = 0.9 -; max = 0.999 -; scale = auto - -; [sweep.train.adam_beta2] -; distribution = logit_normal -; min = 0.99 -; mean = 0.999 -; max = 0.9999 -; scale = auto - -; [sweep.train.adv_sampling_prio_alpha] -; distribution = logit_normal -; min = 0.7 -; mean = 0.85 -; max = 0.99 -; scale = auto - -; [sweep.train.adv_sampling_prio_beta0] -; distribution = logit_normal -; min = 0.7 -; mean = 0.85 -; max = 0.99 -; scale = auto - -[controlled_exp.train.learning_rate] -values = [0.001, 0.003, 0.01] - -[controlled_exp.train.ent_coef] -values = [0.01, 0.005] diff --git a/pufferlib/config/ocean/drive/eval/behaviors.yaml b/pufferlib/config/ocean/drive/eval/behaviors.yaml new file mode 100644 index 0000000000..8ed2ec6bc2 --- /dev/null +++ b/pufferlib/config/ocean/drive/eval/behaviors.yaml @@ -0,0 +1,104 @@ +# Driving-behaviour evaluation: nuPlan scenes labeled by scene type. Each +# behavior is one [eval.behaviors_*] section inheriting the shared template. + +behaviors_defaults: + enabled: false + interval: 250 + mode: inline + clean: true + render: true + render_views: ["sim_state", "bev"] + env: + simulation_mode: replay + control_mode: control_sdc_only + init_mode: create_all_valid + scenario_length: 201 + obs_slots_partners_n: 32 + eval: + num_scenarios: 50 + render_num_scenarios: 2 + render_max_steps: 200 + +behaviors_full_dir: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/nuplan_mini_train_bins + +behaviors_hard_stop: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/hard_stop + +behaviors_highway_straight: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/highway_straight + +behaviors_lane_change: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/lane_change + +behaviors_merge: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/merge + +behaviors_parked_cars: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/parked_cars + +behaviors_roundabout: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/roundabout + +behaviors_stopped_traffic: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/stopped_traffic + +behaviors_traffic_light_green: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/traffic_light_green + +behaviors_traffic_light_stop: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/traffic_light_stop + +behaviors_unprotected_left: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/unprotected_left + +behaviors_unprotected_right: + inherits: behaviors_defaults + type: behavior_class + enabled: true + env: + map_dir: /scratch/ev2237/data/nuplan/categories_v021/unprotected_right diff --git a/pufferlib/config/ocean/drive/eval/validation.yaml b/pufferlib/config/ocean/drive/eval/validation.yaml new file mode 100644 index 0000000000..8f566a86df --- /dev/null +++ b/pufferlib/config/ocean/drive/eval/validation.yaml @@ -0,0 +1,97 @@ +# Multi-scenario validation suite. `validation_defaults` is a template (no +# `type`); the replay/gigaflow/triage sections inherit it and add their +# sim-specific map set + agent layout. Each lands under [eval.]. + +validation_defaults: + enabled: true + interval: 250 + mode: inline + clean: true + env: + eval_mode: 1 + collision_behavior: 1 + offroad_behavior: 1 + # Explicit 0 wins over the clean macro's red-light enforcement. + traffic_light_behavior: 0 + reward_randomization: false + termination_mode: 0 + num_agents: 1024 + target_type: static + goal_speed: 3.0 + reward_collision: 3.0 + reward_offroad: 3.0 + reward_stop_line: 1.0 + reward_goal: 1.0 + reward_overspeed: 0.05 + reward_comfort: 0.05 + reward_velocity: 0.0025 + reward_lane_align: 0.025 + reward_lane_center: 0.0038 + reward_timestep: 0.000025 + reward_reverse: 0.005 + reward_ade: 0.0 + obs_dropout_lane: 0.0 + obs_dropout_boundary: 0.0 + obs_slots_lane_n: 80 + obs_slots_boundary_n: 80 + eval: + num_scenarios: 250 + export_episode_csv: true + verify_coverage: true + +validation_replay: + inherits: validation_defaults + type: multi_scenario + enabled: true + render: true + render_backend: triage_html + env: + simulation_mode: replay + control_mode: control_sdc_only + map_dir: /scratch/ev2237/data/nuplan/nuplan_mini_train_bins + num_maps: 250 + max_agents_per_env: 64 + scenario_length: 200 + resample_frequency: 200 + eval: + render_num_scenarios: 5 + render_max_steps: 200 + +validation_gigaflow: + inherits: validation_defaults + type: multi_scenario + enabled: true + render: true + render_backend: egl + render_views: ["sim_state", "bev"] + env: + simulation_mode: gigaflow + map_dir: pufferlib/resources/drive/binaries/carla + num_maps: 8 + num_agents: 1024 + min_agents_per_env: 40 + max_agents_per_env: 40 + scenario_length: 500 + resample_frequency: 500 + eval: + render_num_scenarios: 8 + render_max_steps: 300 + +dnf_triage: + inherits: validation_defaults + type: multi_scenario + enabled: false + render: true + render_backend: triage_html + env: + simulation_mode: gigaflow + map_dir: pufferlib/resources/drive/binaries/carla/opendrive__Town10HD.bin + num_maps: 1 + min_agents_per_env: 1 + max_agents_per_env: 1 + scenario_length: 500 + resample_frequency: 500 + eval: + num_scenarios: 32 + render_num_scenarios: 16 + render_max_steps: 300 diff --git a/pufferlib/config/ocean/drive/eval/wosac.yaml b/pufferlib/config/ocean/drive/eval/wosac.yaml new file mode 100644 index 0000000000..8373242277 --- /dev/null +++ b/pufferlib/config/ocean/drive/eval/wosac.yaml @@ -0,0 +1,18 @@ +# Optional WOSAC realism eval. Off by default; runs as a subprocess. +wosac: + type: wosac + enabled: false + interval: 500 + mode: subprocess + clean: true + render: false + env: + control_mode: control_wosac + init_mode: create_all_valid + init_step: 10 + goal_radius: 2.0 + eval: + wosac_num_rollouts: 32 + wosac_num_agents: 256 + wosac_sanity_check: false + wosac_aggregate_results: true diff --git a/pufferlib/config/ocean/drive/model/gigaflow.yaml b/pufferlib/config/ocean/drive/model/gigaflow.yaml new file mode 100644 index 0000000000..f80abddd9f --- /dev/null +++ b/pufferlib/config/ocean/drive/model/gigaflow.yaml @@ -0,0 +1,36 @@ +# Policy/network definition (internal sections: [base] net ids, [policy], [rnn]). +base: + policy_name: Drive + # Options: Recurrent, None + rnn_name: null + +policy: + # Encoder layer (per-encoder embedding width) + ego_input_size: 64 + partner_input_size: 64 + lane_input_size: 64 + boundary_input_size: 64 + traffic_control_input_size: 64 + context_input_size: 64 + # Encoder activation - options: "relu", "tanh", "gelu" + encoder_activation: relu + encoder_layer_norm: true + mask_padded_features: false + # Shared backbone layer + backbone_hidden_size: 512 + backbone_num_layers: 4 + # Backbone activation - options: "relu", "tanh", "gelu" + backbone_activation: gelu + backbone_layer_norm: false + # Actor head layer + actor_hidden_size: 512 + actor_num_layers: 0 + # Critic head layer + critic_hidden_size: 512 + critic_num_layers: 0 + # Dual or shared actor-critic backbone + shared_network: true + +rnn: + input_size: 512 + hidden_size: 512 diff --git a/pufferlib/config/ocean/drive/recipes/default.yaml b/pufferlib/config/ocean/drive/recipes/default.yaml new file mode 100644 index 0000000000..2bf6fecab4 --- /dev/null +++ b/pufferlib/config/ocean/drive/recipes/default.yaml @@ -0,0 +1,35 @@ +# Default PufferDrive training recipe. Selects one component per group plus a +# list of eval suites. Swap a group on the CLI, e.g. +# puffer train puffer_drive --config +# or point --config at a sibling recipe to define a different experiment. + +package: ocean +env_name: puffer_drive + +vecenv: default # -> ../vecenv/default.yaml ([vec]) +train: default # -> ../train/default.yaml ([train]) +simulator: gigaflow_carla # -> ../simulator/gigaflow_carla.yaml ([env]) +model: gigaflow # -> ../model/gigaflow.yaml ([base]/[policy]/[rnn]) + +eval: # each -> ../eval/.yaml + - validation + - behaviors + - wosac + +# --- Inline sections (merged verbatim, not from component files) --- + +# Failure-mining workflow: `puffer mine_failures puffer_drive ...` +mine: + output_dir: "" + num_episodes: 100 + score_threshold: -inf + render: true + +# Controlled experiments: `puffer controlled_exp puffer_drive` runs the cross +# product of these value lists. +controlled_exp: + train: + learning_rate: + values: [0.001, 0.003, 0.01] + ent_coef: + values: [0.01, 0.005] diff --git a/pufferlib/config/ocean/drive/recipes/replay_nuplan.yaml b/pufferlib/config/ocean/drive/recipes/replay_nuplan.yaml new file mode 100644 index 0000000000..475a666671 --- /dev/null +++ b/pufferlib/config/ocean/drive/recipes/replay_nuplan.yaml @@ -0,0 +1,25 @@ +# nuPlan replay training: ego SDC learns against log-replayed traffic. +# Same puffer_drive env as the default recipe, swapped to the replay_nuplan +# simulator profile, with validation switched off (empty eval list). +# +# puffer train puffer_drive --config pufferlib/config/ocean/drive/recipes/replay_nuplan.yaml + +package: ocean +env_name: puffer_drive + +vecenv: default +train: default +model: gigaflow +simulator: replay_nuplan # replay mode, nuPlan maps, termination_mode 1, 200 steps + +eval: [] # no validation suites + +# Patch a few values on top of the chosen components. Replay batches are +# smaller than gigaflow, so a smaller minibatch and higher entropy bonus help, +# as in the old replay_sdc config. +overrides: + train: + seed: 555 + ent_coef: 0.19 + minibatch_size: 8192 + checkpoint_interval: 1000 diff --git a/pufferlib/config/ocean/drive/simulator/gigaflow_carla.yaml b/pufferlib/config/ocean/drive/simulator/gigaflow_carla.yaml new file mode 100644 index 0000000000..ab56adb479 --- /dev/null +++ b/pufferlib/config/ocean/drive/simulator/gigaflow_carla.yaml @@ -0,0 +1,125 @@ +# Simulation / environment settings (internal section: [env]). +env: + # --- Simulation --- + # Options: "gigaflow", "replay" + simulation_mode: gigaflow + # Total number of agents in the environment + num_agents: 1024 + # GIGAFLOW-specific: minimum/maximum number of agents per environment + min_agents_per_env: 1 + max_agents_per_env: 80 + # Actions type - options: "discrete", "continuous" + action_type: discrete + # Dynamics model - options: "classic", "jerk" + dynamics_model: jerk + # Time delta between steps in seconds + dt: 0.1 + # Optional nonzero launch speed for gigaflow random spawns + spawn_initial_speed: 0.0 + # Collision behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove + collision_behavior: 1 + # Offroad behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove + offroad_behavior: 1 + # Traffic light behavior - options: 0 - Ignore, 1 - Stop, 2 - Remove + traffic_light_behavior: 1 + # Share static map geometry across envs using the same map - 0 off, 1 on + use_map_cache: 0 + # Number of steps before reset + scenario_length: 1280 + # Frequency of resampling scenario (in steps), 0 to disable + resample_frequency: 128000000 + # End of episode - options: 0 - scenario_length, 1 - early reset when too many agents removed + termination_mode: 1 + # Ratio of inactive agents that triggers early reset (used when termination_mode = 1) + inactive_agent_threshold: 0.4 + # Determines which step of the trajectory to initialize the agents at upon reset + init_step: 0 + # options: "control_vehicles", "control_agents", "control_tracks_to_predict", "control_sdc_only" + control_mode: control_vehicles + # Controller used by agent 0, the canonical SDC/target. + # options: "static", "policy", "replay", "idm" + sdc_controller: policy + # Controller used by non-SDC vehicles. + non_sdc_controller: policy + # Controller used by non-vehicle agents. "auto" follows non_sdc_controller unless it is "idm". + non_vehicle_controller: auto + # Options: "create_all_valid", "create_only_controlled" + init_mode: create_all_valid + # Enable computation of evaluation-only metrics + compute_eval_metrics: false + + # --- Goal / Target --- + # Target representation - options: "static", "dynamic" + target_type: static + # True: place goals along the agent's route. False: scatter at random drivable points. + goal_on_lane: true + # Meters around goal to be considered "reached" + goal_radius: 2.0 + # Maximum speed at final waypoint to count goal reward + goal_speed: 1000.0 + # Number of target waypoints in the observation (including goal) + num_target_waypoints: 3 + # Waypoint spacing range in meters when building path + min_waypoint_spacing: 20.0 + max_waypoint_spacing: 60.0 + + # --- Rewards --- + reward_conditioning: false + reward_randomization: false + reward_goal: 1.0 + reward_collision: 1.5 + reward_offroad: 1.5 + reward_stop_line: 1.0 + reward_comfort: 0.05 + reward_lane_align: 0.025 + reward_vel_align: 1.0 + reward_lane_center: 0.0038 + reward_center_bias: 0.0 + reward_velocity: 0.0025 + reward_reverse: 0.005 + reward_timestep: 0.000025 + reward_overspeed: 0.05 + reward_ade: 0.0 + + # --- Map --- + # Path to map used for training + map_dir: pufferlib/resources/drive/binaries/carla + # Number of maps to load from map_dir + num_maps: 8 + + # --- Observation slot counts --- + obs_slots_lane_n: 80 + obs_slots_boundary_n: 80 + obs_slots_partners_n: 16 + obs_slots_traffic_controls_n: 4 + # Fraction of segment observation slots to drop (reduces obs size) + obs_dropout_lane: 0.0 + obs_dropout_boundary: 0.0 + # Stride for lane and boundary segments; 1 means use every segment, 2 means use every other segment, etc. + obs_lane_stride: 1 + obs_boundary_stride: 1 + # --- Observation normalization --- + obs_norm_goal_offset_m: 120.0 + obs_norm_xy_offset_m: 120.0 + obs_norm_veh_length_m: 15.0 + obs_norm_veh_width_m: 10.0 + obs_norm_road_seg_length_m: 10.0 + obs_norm_road_seg_width_m: 5.0 + # --- Observation ranges (meters, ego frame) --- + obs_range_road_front_m: 120.0 + obs_range_road_behind_m: 20.0 + obs_range_road_side_m: 30.0 + obs_range_partner_m: 100.0 + obs_range_traffic_control_m: 100.0 + + # --- Robustness features --- + # Per-episode probability that an agent is blind to other agents for the whole episode + partner_blindness_prob: 0.0 + # Per-step probability that a flagged blind-partner agent zeros out its partner observations + partner_blindness_trigger_prob: 0.0 + # Per-episode probability that an agent is flagged as a phantom braker + phantom_braking_prob: 0.0 + # Per-step probability that a flagged phantom-braker actually initiates a brake window + phantom_braking_trigger_prob: 0.0 + # Duration (in steps) of phantom braking once triggered + phantom_braking_duration: 10 diff --git a/pufferlib/config/ocean/drive/simulator/replay_nuplan.yaml b/pufferlib/config/ocean/drive/simulator/replay_nuplan.yaml new file mode 100644 index 0000000000..0648031005 --- /dev/null +++ b/pufferlib/config/ocean/drive/simulator/replay_nuplan.yaml @@ -0,0 +1,100 @@ +# Replay-mode simulation on nuPlan maps (internal section: [env]). +# The ego SDC is policy-controlled; all other agents replay their logged +# trajectories. Mirrors the proven validation_replay env, tuned for training. +env: + # --- Simulation --- + simulation_mode: replay + # control_sdc_only: only agent 0 (the ego SDC) is policy-driven; the rest + # are log-replayed. So controlled agents per env == 1, and the number of + # parallel scenarios ~= num_agents / max_agents_per_env. + control_mode: control_sdc_only + # Total agent-slot buffer across all parallel scenarios. With 64 max agents + # per scene this gives ~16 parallel nuPlan scenes / controlled SDCs. Bump + # both num_agents and num_workers/num_envs for more PPO throughput. + num_agents: 1024 + min_agents_per_env: 1 + max_agents_per_env: 64 + action_type: discrete + dynamics_model: jerk + dt: 0.1 + spawn_initial_speed: 0.0 + collision_behavior: 1 + offroad_behavior: 1 + traffic_light_behavior: 1 + use_map_cache: 0 + # 200-step episodes (nuPlan scenes are ~20s @ 0.1s). + scenario_length: 200 + # Resample a fresh scenario every episode so training cycles through maps. + resample_frequency: 200 + # termination_mode 1: early reset once the inactive-agent ratio is exceeded. + termination_mode: 1 + inactive_agent_threshold: 0.4 + init_step: 0 + sdc_controller: policy + non_sdc_controller: replay + non_vehicle_controller: auto + init_mode: create_all_valid + compute_eval_metrics: false + + # --- Goal / Target --- + target_type: static + goal_on_lane: true + goal_radius: 2.0 + goal_speed: 1000.0 + num_target_waypoints: 3 + min_waypoint_spacing: 20.0 + max_waypoint_spacing: 60.0 + + # --- Rewards (collision/offroad bumped to 5.5, as in replay_sdc) --- + reward_conditioning: false + reward_randomization: false + reward_goal: 1.0 + reward_collision: 5.5 + reward_offroad: 5.5 + reward_stop_line: 1.0 + reward_comfort: 0.05 + reward_lane_align: 0.025 + reward_vel_align: 1.0 + reward_lane_center: 0.0038 + reward_center_bias: 0.0 + reward_velocity: 0.1 + reward_reverse: 0.005 + reward_timestep: 0.000025 + reward_overspeed: 0.05 + reward_ade: 0.0 + + # --- Map --- + # Point this at your nuPlan .bin directory. num_maps caps how many are loaded. + map_dir: /scratch/ev2237/data/nuplan/nuplan_mini_train_bins + num_maps: 250 + + # --- Observation slot counts --- + obs_slots_lane_n: 80 + obs_slots_boundary_n: 80 + obs_slots_partners_n: 16 + obs_slots_traffic_controls_n: 4 + obs_dropout_lane: 0.0 + obs_dropout_boundary: 0.0 + # Stride for lane and boundary segments; 1 means use every segment, 2 means use every other segment, etc. + obs_lane_stride: 1 + obs_boundary_stride: 1 + # --- Observation normalization --- + obs_norm_goal_offset_m: 120.0 + obs_norm_xy_offset_m: 120.0 + obs_norm_veh_length_m: 15.0 + obs_norm_veh_width_m: 10.0 + obs_norm_road_seg_length_m: 10.0 + obs_norm_road_seg_width_m: 5.0 + # --- Observation ranges (meters, ego frame) --- + obs_range_road_front_m: 120.0 + obs_range_road_behind_m: 20.0 + obs_range_road_side_m: 30.0 + obs_range_partner_m: 100.0 + obs_range_traffic_control_m: 100.0 + + # --- Robustness features --- + partner_blindness_prob: 0.0 + partner_blindness_trigger_prob: 0.0 + phantom_braking_prob: 0.0 + phantom_braking_trigger_prob: 0.0 + phantom_braking_duration: 10 diff --git a/pufferlib/config/ocean/drive/train/default.yaml b/pufferlib/config/ocean/drive/train/default.yaml new file mode 100644 index 0000000000..e4fccb3f71 --- /dev/null +++ b/pufferlib/config/ocean/drive/train/default.yaml @@ -0,0 +1,42 @@ +# PPO / training-loop settings (internal section: [train]). +train: + total_timesteps: 10000000000 + checkpoint_interval: 50 + anneal_lr: true + # batch_size = num_workers * num_agents * bptt_horizon + batch_size: auto + minibatch_size: 65536 + max_minibatch_size: 65536 + bptt_horizon: 128 + learning_rate: 0.0005 + gae_lambda: 0.95 + gamma: 0.999 + ent_coef: 0.01 + clip_coef: 0.2 + max_grad_norm: 0.5 + vf_clip_coef: null + vf_coef: 0.5 + update_epochs: 2 + normalize_rewards: true + adam_beta1: 0.9 + adam_beta2: 0.999 + adam_eps: 1.0e-8 + vtrace_c_clip: 1 + vtrace_rho_clip: 1 + adv_sampling_prio_alpha: 0.8499999999999999 + adv_sampling_prio_beta0: 0.8499999999999999 + adv_filter_ewma_beta: 0.25 + adv_filter_threshold_scale: 0.01 + # Rendering options + render: false + render_interval: 1000 + # If True, show exactly what the agent sees in agent observation + obs_only: true + # Show grid lines + show_grid: false + # Draws lines from ego agent observed ORUs and road elements to show detection range + show_lasers: false + # Display human xy logs in the background + show_human_logs: true + # Options: List[str to path], str to path, none + render_map: none diff --git a/pufferlib/config/ocean/drive/vecenv/default.yaml b/pufferlib/config/ocean/drive/vecenv/default.yaml new file mode 100644 index 0000000000..42037316ee --- /dev/null +++ b/pufferlib/config/ocean/drive/vecenv/default.yaml @@ -0,0 +1,5 @@ +# Vectorized-env settings (internal section: [vec]). +vec: + num_envs: 20 + num_workers: auto + batch_size: auto diff --git a/pufferlib/config_compose.py b/pufferlib/config_compose.py new file mode 100644 index 0000000000..6f3cc0381b --- /dev/null +++ b/pufferlib/config_compose.py @@ -0,0 +1,262 @@ +"""Composable YAML config (Hydra-style) for PufferDrive. + +A *recipe* is a small .yaml that names which component file to use for each +config group, plus a list of eval suites and any inline overrides: + + # config/ocean/drive/recipes/default.yaml + package: ocean + env_name: puffer_drive + vecenv: default # -> vecenv/default.yaml ([vec]) + train: default # -> train/default.yaml ([train]) + simulator: gigaflow_carla# -> simulator/gigaflow_carla.yaml ([env]) + model: gigaflow # -> model/gigaflow.yaml ([base]/[policy]/[rnn]) + eval: # list, each -> eval/.yaml + - validation + - behaviors + mine: {...} # inline section, merged verbatim + +Each component file is a mapping of *internal section name* -> {keys}, e.g. +`simulator/gigaflow_carla.yaml` contains a top-level `env:` block. The internal +section names (`vec`/`env`/`policy`/`rnn`/`train`/`base`) are deliberately kept +so the composed dict matches exactly what `load_config` already returns and +nothing downstream has to change. + +Composition order (low -> high precedence): + 1. default.ini base layer + 2. the recipe's chosen component files + 3. inline sections written directly in the recipe + (CLI flags are layered on top later, by load_config's argparse.) +""" + +import ast +import configparser +import glob +import os + +import yaml + +# Recipe keys that select a single component file from the matching directory. +# group name -> sub-directory under the component root. +COMPONENT_GROUPS = { + "vecenv": "vecenv", + "train": "train", + "simulator": "simulator", + "model": "model", +} + +# Recipe keys consumed by the composer itself (never copied verbatim as +# config sections). Everything else in a recipe is treated as an inline +# section (e.g. `mine`, `controlled_exp` for those subcommands). +_RECIPE_RESERVED = set(COMPONENT_GROUPS) | {"package", "env_name", "eval", "overrides"} + + +# Lowercase boolean spellings ast.literal_eval can't parse. Centralizing the +# coercion here means native bools reach every consumer (e.g. the evaluators), +# so they never have to defensively re-parse string "true"/"false". +_BOOL_LITERALS = {"true": True, "false": False} + + +def coerce_value(value): + """Config value coercion shared by the .ini loader and argparse CLI parsing. + + Literal-eval Python values (ints, floats, lists, True/False/None), accept + lowercase true/false, else keep the raw string ("auto", "none", paths).""" + if not isinstance(value, str): + return value + if value.strip().lower() in _BOOL_LITERALS: + return _BOOL_LITERALS[value.strip().lower()] + try: + return ast.literal_eval(value) + except (ValueError, SyntaxError): + return value + + +# Backwards-compatible internal alias. +_coerce = coerce_value + + +def _ini_to_nested(ini_path): + """Read an .ini into a nested dict {section: {key: coerced_value}}. + + Section names are kept as-is (including `base`); dotted section names like + `eval.foo` stay flat here and are expanded by the caller if needed.""" + parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) + parser.read(ini_path) + nested = {} + for section in parser.sections(): + nested[section] = {key: _coerce(val) for key, val in parser[section].items()} + return nested + + +def _deep_merge(base, overlay): + """Recursively merge `overlay` into `base` in place; `overlay` wins.""" + for key, value in overlay.items(): + if isinstance(value, dict) and isinstance(base.get(key), dict): + _deep_merge(base[key], value) + else: + base[key] = value + return base + + +def _load_yaml(path): + with open(path) as f: + data = yaml.safe_load(f) + if data is None: + return {} + if not isinstance(data, dict): + raise ValueError(f"Config file {path} must contain a mapping at top level, got {type(data).__name__}") + return data + + +def _load_component(component_root, group_dir, name): + path = os.path.join(component_root, group_dir, f"{name}.yaml") + if not os.path.exists(path): + available = ( + sorted( + os.path.splitext(f)[0] + for f in os.listdir(os.path.join(component_root, group_dir)) + if f.endswith(".yaml") + ) + if os.path.isdir(os.path.join(component_root, group_dir)) + else [] + ) + raise FileNotFoundError(f"Config component '{name}' not found at {path}. Available {group_dir}: {available}") + return _load_yaml(path) + + +def is_recipe(path): + """A recipe is a .yaml/.yml file (vs the legacy .ini config path).""" + return isinstance(path, str) and path.lower().endswith((".yaml", ".yml")) + + +def find_recipe(env_name, puffer_dir): + """Locate the default recipe for `env_name` under config/**/recipes/. + + Returns the path to the recipe whose `env_name` matches, preferring a file + literally named `default.yaml`. Returns None if no recipe matches (caller + then falls back to the legacy .ini loader).""" + pattern = os.path.join(puffer_dir, "config", "**", "recipes", "*.yaml") + matches = [] + for path in glob.glob(pattern, recursive=True): + try: + recipe = _load_yaml(path) + except (ValueError, yaml.YAMLError): + continue + if recipe.get("env_name") == env_name: + matches.append(path) + if not matches: + return None + for path in matches: + if os.path.basename(path) == "default.yaml": + return path + return matches[0] + + +def compose_recipe(recipe_path, default_ini_path): + """Build the nested config dict from a recipe + its component files. + + Output matches load_config's shape: top-level identity keys (package, + env_name, policy_name, rnn_name) hoisted out of `base`, plus section + sub-dicts (vec, train, env, policy, rnn, eval, ...).""" + composed = _ini_to_nested(default_ini_path) + + recipe = _load_yaml(recipe_path) + # Component dirs live alongside the recipes/ dir: /recipes/foo.yaml + component_root = os.path.dirname(os.path.dirname(os.path.abspath(recipe_path))) + + # Identity fields live at the recipe top level. + for key in ("package", "env_name"): + if key in recipe: + composed.setdefault("base", {})[key] = recipe[key] + + # Single-choice component groups: each contributes one or more sections. + for group, group_dir in COMPONENT_GROUPS.items(): + name = recipe.get(group) + if name is None: + continue + if not isinstance(name, str): + raise ValueError( + f"Recipe key '{group}' must name a component file (a string), got {type(name).__name__}. " + f"To tweak individual values, use an `overrides:` block, e.g.\n" + f" overrides:\n {group if group not in ('vecenv', 'simulator', 'model') else 'env'}:\n key: value" + ) + _deep_merge(composed, _load_component(component_root, group_dir, name)) + + # Eval is a list of suites; each file may declare multiple [eval.] + # sections (templates + concrete suites). They all land under `eval`. + composed.setdefault("eval", {}) + for eval_name in recipe.get("eval", []) or []: + suite = _load_component(component_root, "eval", eval_name) + for section_name, body in suite.items(): + composed["eval"][section_name] = body + + # Standalone inline sections (mine, controlled_exp, ...) for non-training + # subcommands. These are whole sections, not patches of a component. + for key, value in recipe.items(): + if key in _RECIPE_RESERVED: + continue + if isinstance(value, dict): + _deep_merge(composed.setdefault(key, {}), value) + else: + composed[key] = value + + # `overrides:` patches any section's values on top of its component — the + # uniform way to tweak a few knobs without forking a component file. Highest + # precedence (CLI flags still win over everything, later). + overrides = recipe.get("overrides") or {} + for section, patch in overrides.items(): + if isinstance(patch, dict): + _deep_merge(composed.setdefault(section, {}), patch) + else: + composed[section] = patch + + # Hoist `base` keys to the top level (load_config registers them as + # bare --flags, e.g. --policy-name, not --base.policy-name). + base_section = composed.pop("base", {}) + for key, value in base_section.items(): + composed[key] = value + + return composed + + +def _read_env_ini(env_name, puffer_dir, default_ini_path): + """Legacy loader: find the .ini whose [base].env_name matches `env_name`, + merged on top of default.ini. Returns a ConfigParser.""" + if env_name == "default": + parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) + parser.read(default_ini_path) + return parser + pattern = os.path.join(puffer_dir, "config", "**", "*.ini") + for path in glob.glob(pattern, recursive=True): + parser = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) + parser.read([default_ini_path, path]) + if parser.has_section("base") and env_name in parser["base"].get("env_name", "").split(): + return parser + raise ValueError(f"No config (recipe or .ini) for env_name {env_name}") + + +def flatten_ini(env_name, puffer_dir, default_ini_path): + """Legacy .ini path -> {dotted_key: coerced_value}, matching the historic + flag scheme: `base` keys are bare, every other section is `section.key`.""" + parser = _read_env_ini(env_name, puffer_dir, default_ini_path) + flat = {} + for section in parser.sections(): + for key in parser[section]: + dotted = key if section == "base" else f"{section}.{key}" + flat[dotted] = _coerce(parser[section][key]) + return flat + + +def flatten_config(nested, prefix=""): + """Nested config dict -> {dotted_key: leaf_value}. Lists are leaves. + + Mirrors the dotted-section flag scheme load_config builds for argparse: + `eval.validation.env.simulation_mode` etc.""" + flat = {} + for key, value in nested.items(): + dotted = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + flat.update(flatten_config(value, dotted)) + else: + flat[dotted] = value + return flat diff --git a/pufferlib/ocean/benchmark/evaluators/base.py b/pufferlib/ocean/benchmark/evaluators/base.py index de51ece27f..9a13b9fc85 100644 --- a/pufferlib/ocean/benchmark/evaluators/base.py +++ b/pufferlib/ocean/benchmark/evaluators/base.py @@ -59,17 +59,14 @@ def __init__(self, name: str, config: dict, train_config: dict): # the base layer that `config` overrides on top of. self.train_config = train_config - # Common scalars pulled out for ergonomics. - raw_enabled = config.get("enabled", True) - if isinstance(raw_enabled, str): - self.enabled: bool = raw_enabled.strip().lower() not in ("false", "0", "no") - else: - self.enabled: bool = bool(raw_enabled) - self.interval: int = int(config.get("interval", 0)) + # Common scalars pulled out for ergonomics. The config layer coerces + # values to native types (bool/int/list), so no re-parsing here. + self.enabled: bool = config.get("enabled", True) + self.interval: int = config.get("interval", 0) self.mode: str = config.get("mode", "inline") - self.render: bool = bool(config.get("render", False)) - self.render_views: list = list(config.get("render_views", ["sim_state"])) - self.clean: bool = bool(config.get("clean", True)) + self.render: bool = config.get("render", False) + self.render_views: list = config.get("render_views", ["sim_state"]) + self.clean: bool = config.get("clean", True) # -- Config hooks --------------------------------------------------- diff --git a/pufferlib/ocean/benchmark/metrics_sanity_check.py b/pufferlib/ocean/benchmark/metrics_sanity_check.py index d0e0e57df8..f5c9f69b8f 100644 --- a/pufferlib/ocean/benchmark/metrics_sanity_check.py +++ b/pufferlib/ocean/benchmark/metrics_sanity_check.py @@ -81,7 +81,7 @@ def format_results_table(results): def main(): parser = argparse.ArgumentParser(description="Validate WOSAC log-likelihood metrics") parser.add_argument("--env", default="puffer_drive") - parser.add_argument("--config", default="config/ocean/drive.ini") + parser.add_argument("--config", default="config/ocean/drive/recipes/default.yaml") args = parser.parse_args() config = load_config(args.env) diff --git a/pufferlib/pufferl.py b/pufferlib/pufferl.py index eeb8364053..7f297b6d20 100644 --- a/pufferlib/pufferl.py +++ b/pufferlib/pufferl.py @@ -37,6 +37,7 @@ from torch.distributed.elastic.multiprocessing.errors import record import pufferlib +import pufferlib.config_compose as config_compose import pufferlib.sweep import pufferlib.utils import pufferlib.vector @@ -2143,6 +2144,9 @@ def load_config(env_name, config_dir=None): parser.add_argument( "--eval_simulation", type=str, default=None, help="Simulation mode for evaluation - gigaflow/replay" ) + parser.add_argument( + "--config", type=str, default=None, help="Path to a composition recipe .yaml (overrides env auto-discovery)" + ) args = parser.parse_known_args()[0] if config_dir is None: @@ -2151,32 +2155,30 @@ def load_config(env_name, config_dir=None): print("Using custom config dir:", config_dir) puffer_dir = config_dir - # Load defaults and config - puffer_config_dir = os.path.join(puffer_dir, "config/**/*.ini") + # Dynamic help menu from config: coerce ini/CLI string values to Python + # (shared with the recipe loader so every source yields native types). + puffer_type = config_compose.coerce_value + puffer_default_config = os.path.join(puffer_dir, "config/default.ini") - if env_name == "default": - p = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) - p.read(puffer_default_config) + + # Resolve the config source. Precedence: + # 1. --config (explicit composition recipe) + # 2. an env-matched recipe under config/**/recipes/ + # 3. the legacy per-env .ini (envs that haven't migrated) + # All three converge on a flat {dotted_key: value} dict that drives the + # argparse registration below — so the returned config shape is identical. + recipe_path = args.config if config_compose.is_recipe(args.config) else None + if recipe_path is None and env_name != "default": + recipe_path = config_compose.find_recipe(env_name, puffer_dir) + + if recipe_path is not None: + composed = config_compose.compose_recipe(recipe_path, puffer_default_config) + flat_defaults = config_compose.flatten_config(composed) else: - for path in glob.glob(puffer_config_dir, recursive=True): - p = configparser.ConfigParser(inline_comment_prefixes=(";", "#")) - p.read([puffer_default_config, path]) - if env_name in p["base"]["env_name"].split(): - break - else: - raise pufferlib.APIUsageError("No config for env_name {}".format(env_name)) + flat_defaults = config_compose.flatten_ini(env_name, puffer_dir, puffer_default_config) - # Dynamic help menu from config - def puffer_type(value): - try: - return ast.literal_eval(value) - except: - return value - - for section in p.sections(): - for key in p[section]: - fmt = f"--{key}" if section == "base" else f"--{section}.{key}" - parser.add_argument(fmt.replace("_", "-"), default=puffer_type(p[section][key]), type=puffer_type) + for dotted_key, value in flat_defaults.items(): + parser.add_argument(f"--{dotted_key}".replace("_", "-"), default=value, type=puffer_type) parser.add_argument( "-h", "--help", default=argparse.SUPPRESS, action="help", help="Show this help message and exit" diff --git a/tests/unit_tests/test_drive_config.py b/tests/unit_tests/test_drive_config.py index 86a7dae082..af696a0a4c 100644 --- a/tests/unit_tests/test_drive_config.py +++ b/tests/unit_tests/test_drive_config.py @@ -31,8 +31,8 @@ def test_load_config(self): """ try: # The ENV_NAME 'puffer_drive' should load config from: - # 1. pufferlib/config/default.ini - # 2. pufferlib/config/ocean/drive.ini (and override defaults) + # 1. pufferlib/config/default.ini (base layer) + # 2. pufferlib/config/ocean/drive/recipes/default.yaml (composed components) args = load_config("puffer_drive") # load_config should return a populated config dict without raising. @@ -75,7 +75,7 @@ def test_checkpoint_arch_merge_keeps_obs_stride(self): @patch("sys.argv", ["pufferl.py", "--train.learning-rate=0.5"]) def test_cli_override(self): """Test that command-line arguments override INI file values.""" - # learning_rate is 0.001 in drive.ini, but we override it to 0.5 here + # learning_rate is set by the recipe's train component, but we override it to 0.5 here args = load_config("puffer_drive") self.assertEqual(args["train"]["learning_rate"], 0.5)