diff --git a/critical/README.md b/critical/README.md
index c298eb85..54890e8f 100644
--- a/critical/README.md
+++ b/critical/README.md
@@ -43,7 +43,7 @@
```bash
# 1. 启动 CARLA 仿真器
-cd D:\hutb\hutb && CarlaUE4.exe
+双击 CarlaUE4.exe
# 2. 运行项目
python main.py
diff --git a/critical/_lane.py b/critical/_lane.py
deleted file mode 100644
index 9cc30aa7..00000000
--- a/critical/_lane.py
+++ /dev/null
@@ -1,114 +0,0 @@
-\section{script\_change\_lane.py}
-
-\begin{verbatim}
- import random
- import re
- import xml.etree.ElementTree as ET
-
- # Function to generate a random integer value within a range
- def random_integer(min_val, max_val):
- return random.randint(min_val, max_val)
-
- # Function to modify the variables in the Python file
- def modify_variables(original_code, original_xml, variable_ranges, num_variations):
- print("Modifying variables...")
- modified_data = []
- for i in range(num_variations): # Change this line
- print(f"Processing variation {i + 1}...")
- modified_code = original_code
- modified_xml = original_xml
-
- for variable, value_spec in variable_ranges.items():
- if value_spec is not None:
- if "-" in value_spec: # Range values
- min_val, max_val = map(int, value_spec.split("-"))
- modified_value = random_integer(min_val, max_val)
- elif "/" in value_spec: # Specific values
- options = value_spec.split("/")
- modified_value = random.choice(options)
- else: # Other specific instructions
- modified_value = value_spec
-
- # Specific condition for self._fast_vehicle_distance
- if variable == "self._fast_vehicle_distance":
- min_diff = 20
- slow_vehicle_distance_pattern = r"self._slow_vehicle_distance\s*=\s*(\d+)"
- slow_vehicle_distance_match = re.search(slow_vehicle_distance_pattern, modified_code)
- if slow_vehicle_distance_match:
- slow_vehicle_distance = int(slow_vehicle_distance_match.group(1))
-
- # Generate a modified value that meets the conditions
- while True:
- modified_value = random.choice([5, 25, 45, 65, 85])
- print(f"Modified value: {modified_value}, Slow vehicle distance: {slow_vehicle_distance}")
- if modified_value < slow_vehicle_distance and slow_vehicle_distance - modified_value >= min_diff:
- print("Condition met. Exiting loop.")
- break
-
- elif variable == "weather":
- options = ["carla.WeatherParameters.ClearNoon", "carla.WeatherParameters.HardRainNoon", "carla.WeatherParameters.ClearNight", "carla.WeatherParameters.HardRainNight"]
- modified_value = random.choice(options)
- weather_part = modified_value.split('.')[-1]
- modified_code = re.sub(r"self\.output\['Weather'\]\s*=\s*\".*\"",
- f"self.output['Weather'] = \"{weather_part}\"", modified_code)
-
- modified_code = re.sub(r'class\s+ChangeLane\s*\(', f'class ChangeLane_{i + 1}(', modified_code)
-
- # Update the first occurrence of the class name in the super constructor
- super_pattern_first = r'super\(ChangeLane, self\)\.__init__\("ChangeLane",'
- modified_code = re.sub(super_pattern_first,
- rf'super(ChangeLane_{i + 1}, self).__init__("ChangeLane_{i + 1}",',
- modified_code, count=1)
-
- # Update the second occurrence of the class name in the super constructor
- super_pattern_second = rf'super\(ChangeLane_{i + 1}, self\)\.__init__\("ChangeLane_{i + 1}",'
- modified_code = re.sub(super_pattern_second,
- rf'super(ChangeLane_{i + 1}, self).__init__("ChangeLane_{i + 1}",',
- modified_code, count=1)
-
- modified_code = re.sub(rf"\b{re.escape(variable)}\b\s*=\s*[^,\n]*", f"{variable} = {modified_value}", modified_code)
-
- modified_code = re.sub(r"self\.output\['Scenario Name'\]\s*=\s*\"ChangeLane\"",
- f"self.output['Scenario Name'] = \"ChangeLane_{i + 1}\"", modified_code)
-
- modified_xml = original_xml.replace('type="ChangeLane"', f'type="ChangeLane_{i + 1}"')
-
- modified_data.append((modified_code, modified_xml))
-
- print("Variables modified successfully.")
- return modified_data
-
- # Read the original Python file
- with open("D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\scenarios\\change_lane.py", "r") as file:
- original_code = file.read()
-
- # Read the original XML file
- with open("D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\examples\\ChangeLane.xml", "r") as file:
- original_xml = file.read()
-
- # Define the variable ranges you want to change
- variable_ranges = {
- "self._fast_vehicle_velocity": "1-32",
- "self._slow_vehicle_distance": "25-160",
- "self._fast_vehicle_distance": "5/25/45/65/85",
- "self._trigger_distance": "10/20/30/40",
- "weather": "carla.WeatherParameters.ClearNoon/carla.WeatherParameters.HardRainNoon/carla.WeatherParameters.ClearNight/carla.WeatherParameters.HardRainNight",
- "desired_speed": "5-116"
- }
-
- # Generate multiple variations by modifying variables
- num_variations = 1000
- print("Generating variations...")
- modified_data = modify_variables(original_code, original_xml, variable_ranges, num_variations)
- print("Variations generated successfully.")
-
- # Write modified versions to separate files
- for i, (modified_code, modified_xml) in enumerate(modified_data):
- print(f"Writing modified files for variation {i + 1}...")
- with open(f"D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\scenarios\\change_lane_{i+1}.py", "w") as code_file:
- code_file.write(modified_code)
-
- with open(f"D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\examples\\ChangeLane_{i + 1}.xml", "w") as xml_file:
- xml_file.write(modified_xml)
- print(f"Modified files for variation {i + 1} written successfully.")
-\end{verbatim}
diff --git a/critical/agent.py b/critical/agent.py
deleted file mode 100644
index b4f4142e..00000000
--- a/critical/agent.py
+++ /dev/null
@@ -1,200 +0,0 @@
-# rl_algorithms/ppo/agent.py
-# 标准 PPO 智能体:Actor-Critic + GAE + 标准裁剪
-
-import numpy as np
-import torch
-import torch.nn as nn
-import torch.optim as optim
-
-from rl_algorithms.base_agent import BaseAgent
-from rl_algorithms.ppo.network import Actor, Critic
-from rl_algorithms.ppo.storage import RolloutStorage
-from rl_algorithms.ppo.clip_utils import standard_clip
-from config.ppo_config import (
- STATE_SIZE, ACTION_SIZE,
- LR_ACTOR, LR_CRITIC,
- GAMMA, LAMBDA, EPS_CLIP,
- UPDATE_EVERY, UPDATE_POLICY_TIMES, BATCH_SIZE,
- VALUE_LOSS_COEF, ENTROPY_COEF, MAX_GRAD_NORM,
-)
-
-
-class PPOAgent(BaseAgent):
- """
- 标准 PPO 智能体(Proximal Policy Optimization)。
-
- 适用场景: #1 大雨跟车, #6 行人横穿, #8 行人闯红灯
- """
-
- def __init__(self, state_size=None, action_size=None):
- state_size = state_size or STATE_SIZE
- action_size = action_size or ACTION_SIZE
- super().__init__(state_size, action_size, name="PPO")
-
- self.actor = Actor(state_size, action_size).to(self.device)
- self.critic = Critic(state_size).to(self.device)
-
- self.actor_opt = optim.Adam(self.actor.parameters(), lr=LR_ACTOR)
- self.critic_opt = optim.Adam(self.critic.parameters(), lr=LR_CRITIC)
-
- self.gamma = GAMMA
- self.lambd = LAMBDA
- self.eps_clip = EPS_CLIP
- self.update_every = UPDATE_EVERY
- self.k_epochs = UPDATE_POLICY_TIMES
- self.batch_size = BATCH_SIZE
- self.value_coef = VALUE_LOSS_COEF
- self.entropy_coef = ENTROPY_COEF
- self.max_grad_norm = MAX_GRAD_NORM
-
- self.storage = RolloutStorage()
-
- # 上次训练的 loss 信息
- self.last_loss_info = {}
-
- # ================================================================
- # 核心接口
- # ================================================================
-
- def act(self, state, evaluate=False):
- """
- 采样动作。返回 (action, log_prob)。
- evaluate=True 时返回概率最高的动作。
- """
- with torch.no_grad():
- state_t = self.to_tensor(state).unsqueeze(0)
- probs = self.actor(state_t)
- dist = torch.distributions.Categorical(probs)
- if evaluate:
- action = probs.argmax(dim=-1)
- else:
- action = dist.sample()
- log_prob = dist.log_prob(action)
- return action.item(), log_prob.item()
-
- def store(self, state, action, log_prob, reward, next_state, done):
- self.storage.push(state, action, log_prob, reward, next_state, done)
-
- def train(self):
- """收集足够步数后执行 PPO 更新"""
- if len(self.storage) < self.update_every:
- return None
-
- self.train_steps += 1
-
- states, actions, old_log_probs, rewards, next_states, dones = \
- self.storage.get_all()
-
- s = self.to_tensor(states)
- a = torch.tensor(actions, dtype=torch.long, device=self.device)
- old_lp = torch.tensor(old_log_probs, dtype=torch.float32, device=self.device)
-
- # 计算 GAE 和 returns
- with torch.no_grad():
- values = self.critic(s).squeeze(-1)
- next_val = self.critic(
- self.to_tensor(next_states[-1:])).squeeze(-1).item()
-
- advantages = self._compute_gae(
- rewards, values.detach().cpu().numpy(), next_val, dones)
- advantages = self.to_tensor(advantages)
- returns = advantages + values.detach()
-
- # 标准化 advantages
- advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
-
- total_actor_loss = 0.0
- total_critic_loss = 0.0
-
- n = len(states)
- for _ in range(self.k_epochs):
- # 小批量训练
- indices = torch.randperm(n)
- for start in range(0, n, self.batch_size):
- idx = indices[start:start + self.batch_size]
-
- s_batch = s[idx]
- a_batch = a[idx]
- old_lp_batch = old_lp[idx]
- adv_batch = advantages[idx]
- ret_batch = returns[idx]
-
- # Actor 损失
- probs = self.actor(s_batch)
- dist = torch.distributions.Categorical(probs)
- new_lp = dist.log_prob(a_batch)
- entropy = dist.entropy().mean()
-
- ratio = torch.exp(new_lp - old_lp_batch)
- clipped = standard_clip(ratio, self.eps_clip)
- actor_loss = -torch.min(
- ratio * adv_batch, clipped * adv_batch).mean()
- actor_loss = actor_loss - self.entropy_coef * entropy
-
- self.actor_opt.zero_grad()
- actor_loss.backward()
- torch.nn.utils.clip_grad_norm_(
- self.actor.parameters(), self.max_grad_norm)
- self.actor_opt.step()
-
- # Critic 损失
- values_pred = self.critic(s_batch).squeeze(-1)
- critic_loss = self.value_coef * nn.MSELoss()(values_pred, ret_batch)
-
- self.critic_opt.zero_grad()
- critic_loss.backward()
- torch.nn.utils.clip_grad_norm_(
- self.critic.parameters(), self.max_grad_norm)
- self.critic_opt.step()
-
- total_actor_loss += actor_loss.item()
- total_critic_loss += critic_loss.item()
-
- self.storage.clear()
- self.last_loss_info = {
- "actor_loss": total_actor_loss / max(self.k_epochs, 1),
- "critic_loss": total_critic_loss / max(self.k_epochs, 1),
- }
- return self.last_loss_info
-
- # ================================================================
- # GAE
- # ================================================================
-
- def _compute_gae(self, rewards, values, next_value, dones):
- """
- 计算 Generalized Advantage Estimation。
-
- rewards: list of float
- values: np.ndarray (T,) V(s_t)
- next_value: float V(s_{T+1})
- dones: np.ndarray (T,)
- """
- T = len(rewards)
- vals = np.append(values, next_value)
- advantages = np.zeros(T, dtype=np.float32)
- gae = 0.0
- for t in reversed(range(T)):
- delta = rewards[t] + self.gamma * vals[t + 1] * (1 - dones[t]) - vals[t]
- gae = delta + self.gamma * self.lambd * (1 - dones[t]) * gae
- advantages[t] = gae
- return advantages
-
- # ================================================================
- # 持久化
- # ================================================================
-
- def _save_checkpoint(self, checkpoint, path):
- checkpoint.update({
- "actor": self.actor.state_dict(),
- "critic": self.critic.state_dict(),
- "actor_opt": self.actor_opt.state_dict(),
- "critic_opt": self.critic_opt.state_dict(),
- })
- torch.save(checkpoint, path)
-
- def _load_checkpoint(self, checkpoint):
- self.actor.load_state_dict(checkpoint["actor"])
- self.critic.load_state_dict(checkpoint["critic"])
- self.actor_opt.load_state_dict(checkpoint["actor_opt"])
- self.critic_opt.load_state_dict(checkpoint["critic_opt"])
diff --git a/critical/combined_night_pedestrian.py b/critical/combined_night_pedestrian.py
deleted file mode 100644
index 0aa8c0e6..00000000
--- a/critical/combined_night_pedestrian.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# 场景9: 夜间行人横穿(耦合) — 晴天夜间亮度5% ego30 行人从人行道黑暗中横穿
-import carla
-from scenarios.base_scenario import BaseScenario
-from utils.carla_utils import spawn_pedestrian_at, walk_to_location, apply_brake
-
-
-class NightPedestrianScenario(BaseScenario):
- def __init__(self):
- super().__init__()
- self.name = "combined_night_pedestrian"
- self.category = "multi_factor_coupled"
- self.weather = carla.WeatherParameters(
- cloudiness=10.0, precipitation=0.0, precipitation_deposits=0.0,
- wind_intensity=0.0, fog_density=0.0, fog_distance=0.0,
- wetness=0.0, sun_azimuth_angle=180.0, sun_altitude_angle=-90.0)
- self.ego_speed_ms = 30.0 / 3.6
-
- def get_env_config(self):
- cfg = super().get_env_config()
- cfg["action_space"] = 2
- return cfg
-
- def _spawn_actors(self):
- self._spawn_ego()
- self._spawn_pedestrian()
- self.world.tick()
-
- def _spawn_scenario_actors_impl(self):
- self._spawn_pedestrian()
-
- def _spawn_pedestrian(self):
- ego_loc = self.ego_vehicle.get_location()
- # 行人从右侧车旁人行道出现(黑暗中无灯光)
- ped_loc = carla.Location(x=ego_loc.x + 20, y=ego_loc.y - 6.0, z=ego_loc.z)
- ped, ctrl = spawn_pedestrian_at(self.world, ped_loc, speed_ms=1.5)
- walk_to_location(ctrl, self.world,
- carla.Location(x=ped_loc.x, y=ego_loc.y + 6.0, z=ped_loc.z))
- self.pedestrians.append((ped, ctrl))
-
- def _control_loop(self):
- braked = False
- for tick in range(int(60 * 20)):
- if not self._running: break
- if braked:
- self.ego_vehicle.apply_control(carla.VehicleControl(throttle=0.0, brake=0.8, steer=0.0))
- else:
- self.ego_vehicle.apply_control(carla.VehicleControl(throttle=0.25, steer=0.0))
- self.world.tick()
- if tick == int(3.0 * 20):
- braked = True
- if tick % 5 == 0: self._record_frame(tick)
- if self.collision_sensor and self.collision_sensor.collided: break
diff --git a/critical/__init__.py b/critical/config/__init__.py
similarity index 100%
rename from critical/__init__.py
rename to critical/config/__init__.py
diff --git a/critical/carla_config.py b/critical/config/carla_config.py
similarity index 100%
rename from critical/carla_config.py
rename to critical/config/carla_config.py
diff --git a/critical/dqn_config.py b/critical/config/dqn_config.py
similarity index 100%
rename from critical/dqn_config.py
rename to critical/config/dqn_config.py
diff --git a/critical/ppo_config.py b/critical/config/ppo_config.py
similarity index 100%
rename from critical/ppo_config.py
rename to critical/config/ppo_config.py
diff --git a/critical/env/__init__.py b/critical/env/__init__.py
new file mode 100644
index 00000000..0a47e571
--- /dev/null
+++ b/critical/env/__init__.py
@@ -0,0 +1,5 @@
+# env/__init__.py
+# 环境模块统一入口
+
+from .carla_env import CarlaEnv
+from .reward import RewardCalculator
diff --git a/critical/carla_env.py b/critical/env/carla_env.py
similarity index 97%
rename from critical/carla_env.py
rename to critical/env/carla_env.py
index d23007dc..824f0a4d 100644
--- a/critical/carla_env.py
+++ b/critical/env/carla_env.py
@@ -9,7 +9,14 @@
try:
import carla
except ImportError:
- carla_root = os.environ.get("CARLA_ROOT", r"D:\hutb\hutb")
+ carla_root = os.environ.get("CARLA_ROOT")
+ if not carla_root:
+ raise ImportError(
+ "未设置 CARLA_ROOT 环境变量。\n"
+ "请设置环境变量指向 CARLA 安装目录,例如:\n"
+ " Windows: set CARLA_ROOT=D:\\hutb\\hutb\n"
+ " 或在代码中: os.environ['CARLA_ROOT'] = 'D:\\hutb\\hutb'"
+ )
egg_dir = os.path.join(carla_root, "PythonAPI", "carla", "dist")
egg_file = os.path.join(egg_dir,
"carla-0.9.16-py3.7-win-amd64.egg" if os.name == "nt"
diff --git a/critical/reward.py b/critical/env/reward.py
similarity index 100%
rename from critical/reward.py
rename to critical/env/reward.py
diff --git a/critical/experiments/__init__.py b/critical/experiments/__init__.py
new file mode 100644
index 00000000..82a19f4e
--- /dev/null
+++ b/critical/experiments/__init__.py
@@ -0,0 +1,8 @@
+# experiments/__init__.py
+
+from .run_dqn import run as run_dqn
+from .run_attention_dqn import run as run_attention_dqn
+from .run_ppo import run as run_ppo
+from .run_smooth_ppo import run as run_smooth_ppo
+from .evaluate import evaluate
+from .compare import compare
diff --git a/critical/compare.py b/critical/experiments/compare.py
similarity index 95%
rename from critical/compare.py
rename to critical/experiments/compare.py
index 3dc7563b..d63dd96d 100644
--- a/critical/compare.py
+++ b/critical/experiments/compare.py
@@ -229,6 +229,21 @@ def _plot_comparison(train_data, eval_data, algo_pair, scenario_name, output_dir
"""生成对比图表(需 matplotlib)"""
import matplotlib
matplotlib.use("Agg")
+ from matplotlib import font_manager
+ # 直接指定字体文件路径,绕过缓存
+ font_paths = [
+ "C:/Windows/Fonts/simhei.ttf",
+ "C:/Windows/Fonts/msyh.ttc",
+ "C:/Windows/Fonts/simsun.ttc",
+ ]
+ for fp in font_paths:
+ try:
+ font_manager.fontManager.addfont(fp)
+ except Exception:
+ pass
+ matplotlib.rcParams["font.family"] = "sans-serif"
+ matplotlib.rcParams["font.sans-serif"] = ["SimHei", "Microsoft YaHei", "SimSun", "DejaVu Sans"]
+ matplotlib.rcParams["axes.unicode_minus"] = False
import matplotlib.pyplot as plt
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
@@ -286,6 +301,13 @@ def _plot_comparison(train_data, eval_data, algo_pair, scenario_name, output_dir
print("图表已保存至: %s" % plot_path)
+def _find_latest(pattern):
+ """在模式中包含通配符时查找最新文件"""
+ import glob
+ matches = sorted(glob.glob(pattern))
+ return matches[-1] if matches else pattern
+
+
# ================================================================
# 入口
# ================================================================
@@ -331,10 +353,3 @@ def _plot_comparison(train_data, eval_data, algo_pair, scenario_name, output_dir
}
compare(algo_pair, args.scenario, log_paths, eval_paths, args.output_dir)
-
-
-def _find_latest(pattern):
- """在模式中包含通配符时查找最新文件"""
- import glob
- matches = sorted(glob.glob(pattern))
- return matches[-1] if matches else pattern
diff --git a/critical/evaluate.py b/critical/experiments/evaluate.py
similarity index 100%
rename from critical/evaluate.py
rename to critical/experiments/evaluate.py
diff --git a/critical/run_attention_dqn.py b/critical/experiments/run_attention_dqn.py
similarity index 100%
rename from critical/run_attention_dqn.py
rename to critical/experiments/run_attention_dqn.py
diff --git a/critical/run_dqn.py b/critical/experiments/run_dqn.py
similarity index 100%
rename from critical/run_dqn.py
rename to critical/experiments/run_dqn.py
diff --git a/critical/run_ppo.py b/critical/experiments/run_ppo.py
similarity index 100%
rename from critical/run_ppo.py
rename to critical/experiments/run_ppo.py
diff --git a/critical/run_smooth_ppo.py b/critical/experiments/run_smooth_ppo.py
similarity index 100%
rename from critical/run_smooth_ppo.py
rename to critical/experiments/run_smooth_ppo.py
diff --git a/critical/jaywalking.py b/critical/jaywalking.py
deleted file mode 100644
index 2cc9aabb..00000000
--- a/critical/jaywalking.py
+++ /dev/null
@@ -1,53 +0,0 @@
-# 场景8: 行人闯红灯 — 晴天白天 ego40绿灯 行人从红绿灯旁人行道闯红灯横穿
-import carla
-from scenarios.base_scenario import BaseScenario
-from utils.carla_utils import spawn_pedestrian_at, walk_to_location, apply_brake
-
-
-class JaywalkingScenario(BaseScenario):
- def __init__(self):
- super().__init__()
- self.name = "jaywalking"
- self.category = "pedestrian_danger"
- self.weather = carla.WeatherParameters(
- cloudiness=5.0, precipitation=0.0, precipitation_deposits=0.0,
- wind_intensity=0.0, fog_density=0.0, fog_distance=0.0,
- wetness=0.0, sun_azimuth_angle=90.0, sun_altitude_angle=45.0)
- self.ego_speed_ms = 40.0 / 3.6
-
- def get_env_config(self):
- cfg = super().get_env_config()
- cfg["action_space"] = 2
- return cfg
-
- def _spawn_actors(self):
- self._spawn_ego()
- self._spawn_pedestrian()
- self.world.tick()
-
- def _spawn_scenario_actors_impl(self):
- self._spawn_pedestrian()
-
- def _spawn_pedestrian(self):
- ego_loc = self.ego_vehicle.get_location()
- # 行人从红绿灯旁左侧人行道闯出
- ped_loc = carla.Location(x=ego_loc.x + 18, y=ego_loc.y + 6.0, z=ego_loc.z)
- ped, ctrl = spawn_pedestrian_at(self.world, ped_loc, speed_ms=1.8)
- # 闯红灯横穿到对面
- walk_to_location(ctrl, self.world,
- carla.Location(x=ped_loc.x, y=ego_loc.y - 6.0, z=ped_loc.z))
- self.pedestrians.append((ped, ctrl))
-
- def _control_loop(self):
- braked = False
- for tick in range(int(60 * 20)):
- if not self._running: break
- if braked:
- self.ego_vehicle.apply_control(carla.VehicleControl(throttle=0.0, brake=0.8, steer=0.0))
- else:
- self.ego_vehicle.apply_control(carla.VehicleControl(throttle=0.3, steer=0.0))
- self.world.tick()
- if tick == int(2.5 * 20):
- braked = True
- if tick % 5 == 0: self._record_frame(tick)
- if self.collision_sensor and self.collision_sensor.collided: break
diff --git a/critical/lane.py b/critical/lane.py
deleted file mode 100644
index bb158f2c..00000000
--- a/critical/lane.py
+++ /dev/null
@@ -1,100 +0,0 @@
-\section{change\_lane.py}
-
- \begin{verbatim}
- import random
- import re
- import xml.etree.ElementTree as ET
-
- # Function to generate a random integer value within a range
- def random_integer(min_val, max_val):
- return random.randint(min_val, max_val)
-
- # Function to modify the variables in the Python file
- def modify_variables(original_code, original_xml, variable_ranges, num_variations):
- print("Modifying variables...")
- modified_data = []
- for i in range(num_variations): # Change this line
- print(f"Processing variation {i + 1}...")
- modified_code = original_code
- modified_xml = original_xml
-
- for variable, value_spec in variable_ranges.items():
- if value_spec is not None:
- if "-" in value_spec: # Range values
- min_val, max_val = map(int, value_spec.split("-"))
- modified_value = random_integer(min_val, max_val)
- elif "/" in value_spec: # Specific values
- options = value_spec.split("/")
- modified_value = random.choice(options)
- else: # Other specific instructions
- modified_value = value_spec
-
- # Specific condition for self._fast_vehicle_distance
- if variable == "self._fast_vehicle_distance":
- min_diff = 20
- slow_vehicle_distance_pattern = r"self._slow_vehicle_distance\s*=\s*(\d+)"
- slow_vehicle_distance_match = re.search(slow_vehicle_distance_pattern, modified_code)
- if slow_vehicle_distance_match:
- slow_vehicle_distance = int(slow_vehicle_distance_match.group(1))
-
- # Generate a modified value that meets the conditions
- while True:
- modified_value = random.choice([5, 25, 45, 65, 85])
- print(f"Modified value: {modified_value}, Slow vehicle distance: {slow_vehicle_distance}")
- if modified_value < slow_vehicle_distance and slow_vehicle_distance - modified_value >= min_diff:
- print("Condition met. Exiting loop.")
- break
-
- elif variable == "weather":
- options = ["carla.WeatherParameters.ClearNoon", "carla.WeatherParameters.HardRainNoon", "carla.WeatherParameters.ClearNight", "carla.WeatherParameters.HardRainNight"]
- modified_value = random.choice(options)
- weather_part = modified_value.split('.')[-1]
- modified_code = re.sub(r"self.output\['Weather']\s*=\s*\"\"\"",
- f"self.output['Weather'] = \"{weather_part}\"", modified_code)
-
- modified_code = re.sub(rf"\b{re.escape(variable)}\b\s*=\s*[^,\n]*", f"{variable} = {modified_value}", modified_code)
-
- modified_code = re.sub(r"self.output\['Scenario Name']\s*=\s*\"ChangeLane\"",
- f"self.output['Scenario Name'] = \"ChangeLane_{i + 1}\"", modified_code)
-
- modified_xml = original_xml.replace('type="ChangeLane"', f'type="ChangeLane_{i + 1}"')
-
- modified_data.append((modified_code, modified_xml))
-
- print("Variables modified successfully.")
- return modified_data
-
- # Read the original Python file
- with open("D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\scenarios\\change_lane.py", "r") as file:
- original_code = file.read()
-
- # Read the original XML file
- with open("D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\examples\\ChangeLane.xml", "r") as file:
- original_xml = file.read()
-
- # Define the variable ranges you want to change
- variable_ranges = {
- "self._fast_vehicle_velocity": "1-32",
- "self._slow_vehicle_distance": "25-160",
- "self._fast_vehicle_distance": "5/25/45/65/85",
- "self._trigger_distance": "10/20/30/40",
- "weather": "carla.WeatherParameters.ClearNoon/carla.WeatherParameters.HardRainNoon/carla.WeatherParameters.ClearNight/carla.WeatherParameters.HardRainNight",
- "desired_speed": "5-116"
- }
-
- # Generate multiple variations by modifying variables
- num_variations = 1000
- print("Generating variations...")
- modified_data = modify_variables(original_code, original_xml, variable_ranges, num_variations)
- print("Variations generated successfully.")
-
- # Write modified versions to separate files
- for i, (modified_code, modified_xml) in enumerate(modified_data):
- print(f"Writing modified files for variation {i + 1}...")
- with open(f"D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\scenarios\\change_lane_{i+1}.py", "w") as code_file:
- code_file.write(modified_code)
-
- with open(f"D:\\BaiduNetdiskDownload\\github\\scenario_runner\\srunner\\examples\\ChangeLane_{i + 1}.xml", "w") as xml_file:
- xml_file.write(modified_xml)
- print(f"Modified files for variation {i + 1} written successfully.")
- \end{verbatim}
diff --git a/critical/main.py b/critical/main.py
index a11d9983..c846aada 100644
--- a/critical/main.py
+++ b/critical/main.py
@@ -54,8 +54,19 @@ def _menu_evaluate():
def _menu_compare():
from experiments.compare import compare
- p = input("对比对 (dqn/ppo): ").strip()
- pair = ("DQN", "Attention-DQN") if p == "dqn" else ("PPO", "Smooth-PPO")
+ print(" 对比对:")
+ print(" dqn = DQN vs Attention-DQN")
+ print(" ppo = PPO vs Smooth-PPO")
+ print(" cross1 = DQN vs PPO")
+ print(" cross2 = Attention-DQN vs Smooth-PPO")
+ p = input("对比对 (dqn/ppo/cross1/cross2): ").strip()
+ pair_map = {
+ "dqn": ("DQN", "Attention-DQN"),
+ "ppo": ("PPO", "Smooth-PPO"),
+ "cross1": ("DQN", "PPO"),
+ "cross2": ("Attention-DQN", "Smooth-PPO"),
+ }
+ pair = pair_map.get(p, pair_map["dqn"])
sc = input("场景: ").strip()
compare(pair, sc, {pair[0]: input("%s CSV: " % pair[0]).strip(),
pair[1]: input("%s CSV: " % pair[1]).strip()},
@@ -84,7 +95,7 @@ def _menu_export_xosc():
def _menu_plot():
- print("\n [1]训练曲线 [2]对比图 [3]俯视图")
+ print("\n [1]训练曲线 [2]对比图 [3]俯视图 [4]分类对比柱状图")
c = input("选择: ").strip()
if c == "1":
from visualization import plot_reward_curve
@@ -103,6 +114,26 @@ def _menu_plot():
from visualization import plot_scenario_topview
try: plot_scenario_topview(sc, "results/plots/%s_topview.png" % sc.name)
finally: sc.cleanup()
+ elif c == "4":
+ from visualization import plot_category_comparison, compute_category_scores
+ a1 = input("算法A名称 (如DQN): ").strip()
+ a2 = input("算法B名称 (如PPO): ").strip()
+ print("输入算法 %s 各场景评估JSON路径(空行结束):" % a1)
+ paths_a = []
+ while True:
+ p = input(" %s JSON: " % a1).strip()
+ if not p: break
+ paths_a.append(p)
+ print("输入算法 %s 各场景评估JSON路径(空行结束):" % a2)
+ paths_b = []
+ while True:
+ p = input(" %s JSON: " % a2).strip()
+ if not p: break
+ paths_b.append(p)
+ scores = compute_category_scores({a1: paths_a, a2: paths_b})
+ plot_category_comparison(scores, {a1: a1, a2: a2},
+ save_path="results/plots/category_comparison.png")
+ print("图表已保存: results/plots/category_comparison.png")
def _menu_test():
@@ -124,7 +155,7 @@ def _menu_test():
sp = p.add_subparsers(dest="cmd")
pt = sp.add_parser("train"); pt.add_argument("--algo", required=True); pt.add_argument("--scenario", required=True); pt.add_argument("--episodes", type=int, default=500)
pe = sp.add_parser("evaluate"); pe.add_argument("--algo", required=True); pe.add_argument("--scenario", required=True); pe.add_argument("--model", required=True); pe.add_argument("--episodes", type=int, default=10)
- pc = sp.add_parser("compare"); pc.add_argument("--pair", required=True, choices=["dqn","ppo"]); pc.add_argument("--scenario", required=True)
+ pc = sp.add_parser("compare"); pc.add_argument("--pair", required=True, choices=["dqn","ppo","cross1","cross2"]); pc.add_argument("--scenario", required=True)
px = sp.add_parser("export"); px.add_argument("--scenario", required=True); px.add_argument("--output", default="results/scenarios")
pt2 = sp.add_parser("test"); pt2.add_argument("--suite", choices=["carla","scenario","agent","all"], default="all")
args = p.parse_args()
@@ -135,7 +166,11 @@ def _menu_test():
"ppo":"experiments.run_ppo","smooth_ppo":"experiments.run_smooth_ppo"}
importlib.import_module(mods[args.algo]).run(args.scenario, args.episodes)
elif args.cmd == "evaluate": from experiments.evaluate import evaluate; evaluate(args.algo, args.scenario, args.model, args.episodes)
- elif args.cmd == "compare": from experiments.compare import compare; pair = ("DQN","Attention-DQN") if args.pair=="dqn" else ("PPO","Smooth-PPO"); compare(pair, args.scenario, {}, {})
+ elif args.cmd == "compare":
+ from experiments.compare import compare
+ pair_map = {"dqn": ("DQN","Attention-DQN"), "ppo": ("PPO","Smooth-PPO"), "cross1": ("DQN","PPO"), "cross2": ("Attention-DQN","Smooth-PPO")}
+ pair = pair_map.get(args.pair, pair_map["dqn"])
+ compare(pair, args.scenario, {}, {})
elif args.cmd == "export": s = create_scenario(args.scenario); s.setup(); print("导出: %s" % export_scenario(s, args.output)); s.cleanup()
elif args.cmd == "test":
if args.suite == "carla": from tests.test_carla_connection import run_all as t; t()
diff --git a/critical/osc_exporter/__init__.py b/critical/osc_exporter/__init__.py
new file mode 100644
index 00000000..5dbd8302
--- /dev/null
+++ b/critical/osc_exporter/__init__.py
@@ -0,0 +1,4 @@
+# osc_exporter/__init__.py
+# OpenSCENARIO 导出模块统一入口
+
+from .xosc_writer import XOSCWriter, export_scenario
diff --git a/critical/osc_exporter/xosc_writer.py b/critical/osc_exporter/xosc_writer.py
new file mode 100644
index 00000000..ae4c2655
--- /dev/null
+++ b/critical/osc_exporter/xosc_writer.py
@@ -0,0 +1,443 @@
+# osc_exporter/xosc_writer.py
+# OpenSCENARIO (.xosc) 场景文件自动生成器
+# 从场景配置、轨迹数据、碰撞事件生成符合 ASAM 1.2 标准的 .xosc 文件
+
+import os
+import re
+import json
+from datetime import datetime
+from xml.dom import minidom
+from xml.etree import ElementTree as ET
+
+
+class XOSCWriter:
+ """
+ OpenSCENARIO 写入器。
+
+ 用法:
+ writer = XOSCWriter()
+ writer.set_scenario_config(config_dict)
+ writer.set_trajectory(ego_logs, adv_logs)
+ writer.set_weather(weather_dict)
+ writer.add_collision_event(time_s, location)
+ writer.add_pedestrian(ped_data)
+ xosc_str = writer.generate()
+ writer.save("results/scenarios/my_scenario.xosc")
+ """
+
+ def __init__(self, template_path=None):
+ if template_path is None:
+ template_path = os.path.join(os.path.dirname(__file__), "template.xosc")
+ with open(template_path, "r", encoding="utf-8") as f:
+ self.template = f.read()
+
+ # 待填充的数据
+ self._context = {
+ # 文件头
+ "date": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
+ "description": "CARLA Extreme Driving Scenario",
+ "author": "CARLA RL Scenario Generator",
+
+ # 参数
+ "ego_speed_target": "13.89",
+ "adv_speed_target": "13.89",
+ "initial_distance": "20.0",
+
+ # 地图
+ "map_name": "Town10HD",
+
+ # 自车
+ "ego_model": "vehicle.tesla.model3",
+ "ego_max_speed": "80.0",
+ "ego_init_x": "0.0",
+ "ego_init_y": "0.0",
+ "ego_init_z": "0.3",
+ "ego_init_h": "0.0",
+ "ego_init_speed": "0.0",
+
+ # 对抗车辆
+ "adv_model": "vehicle.audi.a2",
+ "adv_max_speed": "70.0",
+ "adv_init_x": "20.0",
+ "adv_init_y": "0.0",
+ "adv_init_z": "0.3",
+ "adv_init_h": "0.0",
+ "adv_init_speed": "0.0",
+
+ # 天气
+ "weather_cloud": "skyClear",
+ "precipitation_intensity": "0.0",
+ "fog_visual_range": "100000.0",
+ "date_time": datetime.now().strftime("%Y-%m-%dT%H:%M:%S"),
+
+ # 触发
+ "trigger_time": "3.0",
+ "trigger_speed": "0.0",
+
+ # 终止
+ "collision_target": "AdvVehicle",
+ "episode_timeout": "60.0",
+ }
+
+ self._pedestrians = []
+ self._has_trigger = False
+ self._has_pedestrian_event = False
+
+ # ================================================================
+ # 数据设置
+ # ================================================================
+
+ def set_scenario_config(self, config):
+ """
+ 从场景配置 dict 填充基础参数。
+ config 来自 scenario_configs.yaml 中的某个场景定义。
+ """
+ cfg = config or {}
+
+ # 基础信息
+ self._context["description"] = cfg.get("name", self._context["description"])
+ self._context["initial_distance"] = str(cfg.get("initial_distance", 20.0))
+
+ # 速度
+ ego_speed = cfg.get("ego_speed", 40)
+ adv_speed = cfg.get("adv_speed", 35)
+ self._context["ego_speed_target"] = str(ego_speed / 3.6)
+ self._context["adv_speed_target"] = str(adv_speed / 3.6)
+ self._context["ego_init_speed"] = str(ego_speed / 3.6)
+ self._context["adv_init_speed"] = str(adv_speed / 3.6)
+
+ # 类别
+ self._context["category"] = cfg.get("category", "")
+
+ # 行为(急刹 / 加塞)
+ behavior = cfg.get("behavior", {})
+ if behavior:
+ if "brake_deceleration" in behavior:
+ self._context["trigger_time"] = "3.0"
+ self._context["trigger_speed"] = "0.0"
+ self._has_trigger = True
+ if "cut_in_angle" in behavior:
+ self._context["trigger_time"] = "2.0"
+ self._context["trigger_speed"] = str(adv_speed / 3.6)
+ self._has_trigger = True
+
+ # 行人
+ ped_cfg = cfg.get("pedestrian", {})
+ if ped_cfg:
+ self.add_pedestrian({
+ "name": "Pedestrian01",
+ "init_x": str(cfg.get("ego_speed", 40) / 3.6 * 1.5),
+ "init_y": "-4.0",
+ "init_h": "90.0",
+ "target_x": str(cfg.get("ego_speed", 40) / 3.6 * 1.5),
+ "target_y": "4.0",
+ "trigger_time": str(ped_cfg.get("trigger_time", 2.0)),
+ "speed": str(ped_cfg.get("cross_speed", 1.5)),
+ })
+
+ return self
+
+ def set_weather(self, weather_dict):
+ """设置天气参数"""
+ if not weather_dict:
+ return self
+ precip = weather_dict.get("precipitation", 0)
+ fog_dist = weather_dict.get("fog_distance", 0)
+ fog_density = weather_dict.get("fog_density", 0)
+
+ self._context["precipitation_intensity"] = str(precip / 100.0)
+ self._context["fog_visual_range"] = (
+ str(fog_dist) if fog_dist > 0 else "100000.0")
+ if precip > 50:
+ self._context["weather_cloud"] = "overcast"
+ elif precip > 20:
+ self._context["weather_cloud"] = "cloudy"
+ else:
+ self._context["weather_cloud"] = "skyClear"
+ return self
+
+ def set_entities(self, ego_vehicle=None, adv_vehicle=None):
+ """根据实际车辆设置实体初始状态"""
+ if ego_vehicle is not None:
+ loc = ego_vehicle.get_location()
+ vel = ego_vehicle.get_velocity()
+ rot = ego_vehicle.get_transform().rotation
+ self._context["ego_init_x"] = str(round(loc.x, 3))
+ self._context["ego_init_y"] = str(round(loc.y, 3))
+ self._context["ego_init_z"] = str(round(loc.z, 3))
+ self._context["ego_init_h"] = str(round(rot.yaw, 4))
+ speed = (vel.x ** 2 + vel.y ** 2 + vel.z ** 2) ** 0.5
+ self._context["ego_init_speed"] = str(round(speed, 3))
+
+ if adv_vehicle is not None:
+ loc = adv_vehicle.get_location()
+ vel = adv_vehicle.get_velocity()
+ rot = adv_vehicle.get_transform().rotation
+ self._context["adv_init_x"] = str(round(loc.x, 3))
+ self._context["adv_init_y"] = str(round(loc.y, 3))
+ self._context["adv_init_z"] = str(round(loc.z, 3))
+ self._context["adv_init_h"] = str(round(rot.yaw, 4))
+ speed = (vel.x ** 2 + vel.y ** 2 + vel.z ** 2) ** 0.5
+ self._context["adv_init_speed"] = str(round(speed, 3))
+ return self
+
+ def set_trajectory(self, ego_logs, adv_logs=None):
+ """
+ 设置车辆轨迹数据(当前版本记录为注释元数据)。
+ ego_logs: list of (time, x, y, z, speed_kmh) 或更多字段
+ """
+ self._ego_trajectory = ego_logs or []
+ self._adv_trajectory = adv_logs or []
+ return self
+
+ def add_collision_event(self, time_s, location=None):
+ """记录碰撞事件"""
+ self._collision_time = time_s
+ self._collision_location = location
+ return self
+
+ def add_pedestrian(self, ped_data):
+ """
+ 添加行人实体。
+
+ ped_data: dict:
+ name, init_x, init_y, init_h,
+ target_x, target_y,
+ trigger_time (s), speed (m/s)
+ """
+ self._pedestrians.append(ped_data)
+ self._has_pedestrian_event = True
+ return self
+
+ def set_danger_event(self, trigger_time_s, description):
+ """设置危险事件触发时间"""
+ self._has_trigger = True
+ self._context["trigger_time"] = str(trigger_time_s)
+ self._context["description"] = description
+ return self
+
+ # ================================================================
+ # 模板渲染
+ # ================================================================
+
+ def generate(self):
+ """
+ 渲染模板为完整 .xosc XML 字符串。
+ """
+ content = self.template
+
+ # 处理 pedestrians 重复段落
+ ped_block = self._render_pedestrians_block()
+ content = self._replace_section(content, "pedestrians", ped_block)
+
+ # 处理 trigger_event 条件段落
+ if self._has_trigger:
+ trigger_block = self._render_trigger_block()
+ else:
+ trigger_block = ""
+ content = self._replace_section(content, "trigger_event", trigger_block)
+
+ # 处理 pedestrian_event 条件段落
+ if self._has_pedestrian_event and self._pedestrians:
+ ped_event_block = self._render_pedestrian_event_block()
+ else:
+ ped_event_block = ""
+ content = self._replace_section(content, "pedestrian_event", ped_event_block)
+
+ # 替换简单占位符
+ for key, value in self._context.items():
+ placeholder = "{{%s}}" % key
+ content = content.replace(placeholder, str(value))
+
+ return self._pretty_print(content)
+
+ def _render_pedestrians_block(self):
+ """渲染行人实体列表"""
+ if not self._pedestrians:
+ return ""
+ lines = []
+ for p in self._pedestrians:
+ lines.append(
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' ' % (p["name"], p["name"])
+ )
+ return "\n".join(lines)
+
+ def _render_trigger_block(self):
+ """渲染危险触发 Maneuver"""
+ return (
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' '
+ )
+
+ def _render_pedestrian_event_block(self):
+ """渲染行人横穿事件"""
+ if not self._pedestrians:
+ return ""
+ p = self._pedestrians[0]
+ return (
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' \n'
+ ' '
+ % (p["name"], p["target_x"], p["target_y"], p.get("trigger_time", "2.0"))
+ )
+
+ @staticmethod
+ def _replace_section(content, section_name, replacement):
+ """
+ 替换模板中 {{#section_name}}...{{/section_name}} 段落。
+
+ 若 replacement 为空字符串,则删除整个段落。
+ """
+ # 非空时:去掉标记,保留内部内容(已由调用方生成)
+ start_tag = "{{#%s}}" % section_name
+ end_tag = "{{/%s}}" % section_name
+
+ # 找到标签位置
+ start_idx = content.find(start_tag)
+ if start_idx == -1:
+ return content
+ end_idx = content.find(end_tag)
+ if end_idx == -1:
+ return content
+
+ # 如果 replacement 为空 → 删除整个段落
+ # 如果 replacement 非空 → 用 replacement 替代标记包围区域
+ if replacement:
+ # 找到 start_tag 所在行的行首
+ line_start = content.rfind("\n", 0, start_idx)
+ if line_start == -1:
+ line_start = 0
+ else:
+ line_start += 1
+
+ # 找到 end_tag 所在行的行尾
+ line_end = content.find("\n", end_idx)
+ if line_end == -1:
+ line_end = len(content)
+
+ before = content[:line_start]
+ after = content[line_end + 1:] if line_end < len(content) else ""
+ return before + replacement + after
+ else:
+ # 删除整个段落
+ line_start = content.rfind("\n", 0, start_idx)
+ if line_start == -1:
+ line_start = 0
+ line_end = content.find("\n", end_idx)
+ if line_end == -1:
+ line_end = len(content) - 1
+ before = content[:line_start]
+ after = content[line_end + 1:] if line_end < len(content) else ""
+ return before + after
+
+ @staticmethod
+ def _pretty_print(xml_string):
+ """格式化 XML 输出"""
+ try:
+ dom = minidom.parseString(xml_string)
+ return dom.toprettyxml(indent=" ", encoding="UTF-8").decode("utf-8")
+ except Exception:
+ return xml_string
+
+ # ================================================================
+ # 保存
+ # ================================================================
+
+ def save(self, filepath):
+ """生成并保存 .xosc 文件到指定路径"""
+ os.makedirs(os.path.dirname(filepath), exist_ok=True)
+ content = self.generate()
+ with open(filepath, "w", encoding="utf-8") as f:
+ f.write(content)
+ return filepath
+
+
+# ================================================================
+# 快捷函数
+# ================================================================
+
+def export_scenario(scenario, output_dir="results/scenarios"):
+ """
+ 一键导出场景为 .xosc。
+
+ scenario: BaseScenario 子类实例(必须已 setup)
+ output_dir: 输出目录
+ """
+ writer = XOSCWriter()
+
+ # 从场景实例直接获取天气参数
+ if hasattr(scenario, "weather") and scenario.weather is not None:
+ w = scenario.weather
+ weather_dict = {
+ "precipitation": getattr(w, "precipitation", 0),
+ "fog_distance": getattr(w, "fog_distance", 0),
+ "fog_density": getattr(w, "fog_density", 0),
+ }
+ writer.set_weather(weather_dict)
+ writer.set_scenario_config({"name": scenario.name, "ego_speed": scenario.ego_speed_ms * 3.6})
+
+ if scenario.ego_vehicle is not None:
+ writer.set_entities(scenario.ego_vehicle, scenario.adv_vehicle)
+
+ if scenario.logs:
+ writer.set_trajectory(scenario.logs)
+
+ filename = "%s.xosc" % scenario.name
+ filepath = os.path.join(output_dir, filename)
+ return writer.save(filepath)
diff --git a/critical/rl_algorithms/__init__.py b/critical/rl_algorithms/__init__.py
new file mode 100644
index 00000000..9192d931
--- /dev/null
+++ b/critical/rl_algorithms/__init__.py
@@ -0,0 +1,27 @@
+# rl_algorithms/__init__.py
+# 强化学习算法模块统一入口
+
+from .base_agent import BaseAgent
+
+from .dqn.dqn_agent import DQNAgent
+from .dqn.attention_agent import AttentionDQNAgent
+
+from .ppo.ppo_agent import PPOAgent
+from .ppo.smooth_agent import SmoothPPOAgent
+
+# 算法注册表
+ALGORITHM_REGISTRY = {
+ "dqn": DQNAgent,
+ "attention_dqn": AttentionDQNAgent,
+ "ppo": PPOAgent,
+ "smooth_ppo": SmoothPPOAgent,
+}
+
+
+def create_agent(algo_name, state_size=None, action_size=None):
+ """根据名称创建智能体实例"""
+ cls = ALGORITHM_REGISTRY.get(algo_name.lower())
+ if cls is None:
+ raise KeyError("未知算法: %s,可用: %s"
+ % (algo_name, list(ALGORITHM_REGISTRY.keys())))
+ return cls(state_size=state_size, action_size=action_size)
diff --git a/critical/base_agent.py b/critical/rl_algorithms/base_agent.py
similarity index 100%
rename from critical/base_agent.py
rename to critical/rl_algorithms/base_agent.py
diff --git a/critical/rl_algorithms/dqn/__init__.py b/critical/rl_algorithms/dqn/__init__.py
new file mode 100644
index 00000000..f9aa753f
--- /dev/null
+++ b/critical/rl_algorithms/dqn/__init__.py
@@ -0,0 +1,8 @@
+# rl_algorithms/dqn/__init__.py
+# DQN 系列算法
+
+from .dqn_agent import DQNAgent
+from .attention_agent import AttentionDQNAgent
+from .network import QNetwork
+from .attention_network import AttentionQNetwork
+from .replay_buffer import ReplayBuffer
diff --git a/critical/attention_agent.py b/critical/rl_algorithms/dqn/attention_agent.py
similarity index 100%
rename from critical/attention_agent.py
rename to critical/rl_algorithms/dqn/attention_agent.py
diff --git a/critical/attention_network.py b/critical/rl_algorithms/dqn/attention_network.py
similarity index 100%
rename from critical/attention_network.py
rename to critical/rl_algorithms/dqn/attention_network.py
diff --git a/critical/dqn_agent.py b/critical/rl_algorithms/dqn/dqn_agent.py
similarity index 100%
rename from critical/dqn_agent.py
rename to critical/rl_algorithms/dqn/dqn_agent.py
diff --git a/critical/rl_algorithms/dqn/network.py b/critical/rl_algorithms/dqn/network.py
new file mode 100644
index 00000000..4800e6d2
--- /dev/null
+++ b/critical/rl_algorithms/dqn/network.py
@@ -0,0 +1,34 @@
+# rl_algorithms/dqn/network.py
+# 标准 DQN Q 网络结构
+
+import torch
+import torch.nn as nn
+
+from config.dqn_config import STATE_SIZE, ACTION_SIZE, HIDDEN_SIZES, ACTIVATION
+
+
+def _build_mlp(in_dim, out_dim, hidden_sizes, activation="relu"):
+ """构建多层感知机"""
+ layers = []
+ prev = in_dim
+ act_fn = nn.ReLU() if activation == "relu" else nn.Tanh()
+ for h in hidden_sizes:
+ layers.append(nn.Linear(prev, h))
+ layers.append(act_fn)
+ prev = h
+ layers.append(nn.Linear(prev, out_dim))
+ return nn.Sequential(*layers)
+
+
+class QNetwork(nn.Module):
+ """标准 DQN Q 网络"""
+
+ def __init__(self, state_size=None, action_size=None, hidden_sizes=None):
+ super().__init__()
+ self.state_size = state_size or STATE_SIZE
+ self.action_size = action_size or ACTION_SIZE
+ hs = hidden_sizes or HIDDEN_SIZES
+ self.net = _build_mlp(self.state_size, self.action_size, hs, ACTIVATION)
+
+ def forward(self, x):
+ return self.net(x)
diff --git a/critical/replay_buffer.py b/critical/rl_algorithms/dqn/replay_buffer.py
similarity index 100%
rename from critical/replay_buffer.py
rename to critical/rl_algorithms/dqn/replay_buffer.py
diff --git a/critical/rl_algorithms/ppo/__init__.py b/critical/rl_algorithms/ppo/__init__.py
new file mode 100644
index 00000000..257e799e
--- /dev/null
+++ b/critical/rl_algorithms/ppo/__init__.py
@@ -0,0 +1,9 @@
+# rl_algorithms/ppo/__init__.py
+# PPO 系列算法
+
+from .ppo_agent import PPOAgent
+from .smooth_agent import SmoothPPOAgent
+from .network import Actor, Critic
+from .smooth_network import SmoothActor, SmoothCritic
+from .storage import RolloutStorage
+from .clip_utils import standard_clip, smooth_clip
diff --git a/critical/clip_utils.py b/critical/rl_algorithms/ppo/clip_utils.py
similarity index 100%
rename from critical/clip_utils.py
rename to critical/rl_algorithms/ppo/clip_utils.py
diff --git a/critical/network.py b/critical/rl_algorithms/ppo/network.py
similarity index 100%
rename from critical/network.py
rename to critical/rl_algorithms/ppo/network.py
diff --git a/critical/ppo_agent.py b/critical/rl_algorithms/ppo/ppo_agent.py
similarity index 100%
rename from critical/ppo_agent.py
rename to critical/rl_algorithms/ppo/ppo_agent.py
diff --git a/critical/smooth_agent.py b/critical/rl_algorithms/ppo/smooth_agent.py
similarity index 100%
rename from critical/smooth_agent.py
rename to critical/rl_algorithms/ppo/smooth_agent.py
diff --git a/critical/smooth_network.py b/critical/rl_algorithms/ppo/smooth_network.py
similarity index 100%
rename from critical/smooth_network.py
rename to critical/rl_algorithms/ppo/smooth_network.py
diff --git a/critical/storage.py b/critical/rl_algorithms/ppo/storage.py
similarity index 100%
rename from critical/storage.py
rename to critical/rl_algorithms/ppo/storage.py
diff --git a/critical/runner.py b/critical/runner.py
deleted file mode 100644
index 7a8d24dc..00000000
--- a/critical/runner.py
+++ /dev/null
@@ -1,649 +0,0 @@
-\section{scenario\_runner.py}
-
- \begin{verbatim}
- #!/usr/bin/env python
-
- # Copyright (c) 2018-2020 Intel Corporation
- #
- # This work is licensed under the terms of the MIT license.
- # For a copy, see .
-
- """
- Welcome to CARLA scenario_runner
-
- This is the main script to be executed when running a scenario.
- It loads the scenario configuration, loads the scenario and manager,
- and finally triggers the scenario execution.
- """
-
- from __future__ import print_function
-
- import glob
- import traceback
- import argparse
- from argparse import RawTextHelpFormatter
- from datetime import datetime
- from distutils.version import LooseVersion
- import importlib
- import inspect
- import os
- import signal
- import sys
- import time
- import json
- import pkg_resources
-
- import carla
-
- from srunner.scenarioconfigs.openscenario_configuration import OpenScenarioConfiguration
- from srunner.scenariomanager.carla_data_provider import CarlaDataProvider
- from srunner.scenariomanager.scenario_manager import ScenarioManager
- from srunner.scenarios.open_scenario import OpenScenario
- from srunner.scenarios.route_scenario import RouteScenario
- from srunner.tools.scenario_parser import ScenarioConfigurationParser
- from srunner.tools.route_parser import RouteParser
- from srunner.tools.osc2_helper import OSC2Helper
- from srunner.scenarios.osc2_scenario import OSC2Scenario
- from srunner.scenarioconfigs.osc2_scenario_configuration import OSC2ScenarioConfiguration
-
- # Version of scenario_runner
- VERSION = '0.9.13'
-
- class ScenarioRunner(object):
-
- """
- This is the core scenario runner module. It is responsible for
- running (and repeating) a single scenario or a list of scenarios.
-
- Usage:
- scenario_runner = ScenarioRunner(args)
- scenario_runner.run()
- del scenario_runner
- """
-
- ego_vehicles = []
-
- # Tunable parameters
- client_timeout = 10.0 # in seconds
- wait_for_world = 20.0 # in seconds
- frame_rate = 20.0 # in Hz
-
- # CARLA world and scenario handlers
- world = None
- manager = None
-
- finished = False
-
- additional_scenario_module = None
-
- agent_instance = None
- module_agent = None
-
- def __init__(self, args):
- """
- Setup CARLA client and world
- Setup ScenarioManager
- """
- self._args = args
-
- if args.timeout:
- self.client_timeout = float(args.timeout)
-
- # First of all, we need to create the client that will send the requests
- # to the simulator. Here we'll assume the simulator is accepting
- # requests in the localhost at port 2000.
- self.client = carla.Client(args.host, int(args.port))
- self.client.set_timeout(self.client_timeout)
- dist = pkg_resources.get_distribution("carla")
- if LooseVersion(dist.version) < LooseVersion('0.9.15'):
- raise ImportError("CARLA version 0.9.15 or newer required. CARLA version found: {}".format(dist))
-
- # Load agent if requested via command line args
- # If something goes wrong an exception will be thrown by importlib (ok here)
- if self._args.agent is not None:
- module_name = os.path.basename(args.agent).split('.')[0]
- sys.path.insert(0, os.path.dirname(args.agent))
- self.module_agent = importlib.import_module(module_name)
-
- # Create the ScenarioManager
- self.manager = ScenarioManager(self._args.debug, self._args.sync, self._args.timeout)
-
- # Create signal handler for SIGINT
- self._shutdown_requested = False
- if sys.platform != 'win32':
- signal.signal(signal.SIGHUP, self._signal_handler)
- signal.signal(signal.SIGINT, self._signal_handler)
- signal.signal(signal.SIGTERM, self._signal_handler)
-
- self._start_wall_time = datetime.now()
-
- def destroy(self):
- """
- Cleanup and delete actors, ScenarioManager and CARLA world
- """
-
- self._cleanup()
- if self.manager is not None:
- del self.manager
- if self.world is not None:
- del self.world
- if self.client is not None:
- del self.client
-
- def _signal_handler(self, signum, frame):
- """
- Terminate scenario ticking when receiving a signal interrupt
- """
- self._shutdown_requested = True
- if self.manager:
- self.manager.stop_scenario()
- self._cleanup()
- if not self.manager.get_running_status():
- raise RuntimeError("Timeout occurred during scenario execution")
-
- def _get_scenario_class_or_fail(self, scenario):
- """
- Get scenario class by scenario name
- If scenario is not supported or not found, exit script
- """
-
- # Path of all scenario at "srunner/scenarios" folder + the path of the additional scenario argument
- scenarios_list = glob.glob("{}/srunner/scenarios/*.py".format(os.getenv('SCENARIO_RUNNER_ROOT', "./")))
- scenarios_list.append(self._args.additionalScenario)
-
- for scenario_file in scenarios_list:
-
- # Get their module
- module_name = os.path.basename(scenario_file).split('.')[0]
- sys.path.insert(0, os.path.dirname(scenario_file))
- scenario_module = importlib.import_module(module_name)
-
- # And their members of type class
- for member in inspect.getmembers(scenario_module, inspect.isclass):
- if scenario in member:
- return member[1]
-
- # Remove unused Python paths
- sys.path.pop(0)
-
- print("Scenario '{}' not supported ... Exiting".format(scenario))
- sys.exit(-1)
-
- def _cleanup(self):
- """
- Remove and destroy all actors
- """
- if self.finished:
- return
-
- self.finished = True
-
- # Simulation still running and in synchronous mode?
- if self.world is not None and self._args.sync:
- try:
- # Reset to asynchronous mode
- settings = self.world.get_settings()
- settings.synchronous_mode = False
- settings.fixed_delta_seconds = None
- self.world.apply_settings(settings)
- self.client.get_trafficmanager(int(self._args.trafficManagerPort)).set_synchronous_mode(False)
- except RuntimeError:
- sys.exit(-1)
-
- self.manager.cleanup()
-
- CarlaDataProvider.cleanup()
-
- for i, _ in enumerate(self.ego_vehicles):
- if self.ego_vehicles[i]:
- if not self._args.waitForEgo and self.ego_vehicles[i] is not None and self.ego_vehicles[i].is_alive:
- print("Destroying ego vehicle {}".format(self.ego_vehicles[i].id))
- self.ego_vehicles[i].destroy()
- self.ego_vehicles[i] = None
- self.ego_vehicles = []
-
- if self.agent_instance:
- self.agent_instance.destroy()
- self.agent_instance = None
-
- def _prepare_ego_vehicles(self, ego_vehicles):
- """
- Spawn or update the ego vehicles
- """
-
- if not self._args.waitForEgo:
- for vehicle in ego_vehicles:
- self.ego_vehicles.append(CarlaDataProvider.request_new_actor(vehicle.model,
- vehicle.transform,
- vehicle.rolename,
- random_location=vehicle.random_location,
- color=vehicle.color,
- actor_category=vehicle.category))
- else:
- ego_vehicle_missing = True
- while ego_vehicle_missing:
- self.ego_vehicles = []
- ego_vehicle_missing = False
- for ego_vehicle in ego_vehicles:
- ego_vehicle_found = False
- carla_vehicles = CarlaDataProvider.get_world().get_actors().filter('vehicle.*')
- for carla_vehicle in carla_vehicles:
- if carla_vehicle.attributes['role_name'] == ego_vehicle.rolename:
- ego_vehicle_found = True
- self.ego_vehicles.append(carla_vehicle)
- break
- if not ego_vehicle_found:
- ego_vehicle_missing = True
- break
-
- for i, _ in enumerate(self.ego_vehicles):
- self.ego_vehicles[i].set_transform(ego_vehicles[i].transform)
- self.ego_vehicles[i].set_target_velocity(carla.Vector3D())
- self.ego_vehicles[i].set_target_angular_velocity(carla.Vector3D())
- self.ego_vehicles[i].apply_control(carla.VehicleControl())
- CarlaDataProvider.register_actor(self.ego_vehicles[i], ego_vehicles[i].transform)
-
- # sync state
- if CarlaDataProvider.is_sync_mode():
- self.world.tick()
- else:
- self.world.wait_for_tick()
-
- def _analyze_scenario(self, config):
- """
- Provide feedback about success/failure of a scenario
- """
-
- # Create the filename
- current_time = str(datetime.now().strftime('%Y-%m-%d-%H-%M-%S'))
- junit_filename = None
- json_filename = None
- config_name = config.name
- if self._args.outputDir != '':
- config_name = os.path.join(self._args.outputDir, config_name)
-
- if self._args.junit:
- junit_filename = config_name + current_time + ".xml"
- if self._args.json:
- json_filename = config_name + current_time + ".json"
- filename = None
- if self._args.file:
- filename = config_name + current_time + ".txt"
-
- if not self.manager.analyze_scenario(self._args.output, filename, junit_filename, json_filename):
- print("All scenario tests were passed successfully!")
- else:
- print("Not all scenario tests were successful")
- if not (self._args.output or filename or junit_filename):
- print("Please run with --output for further information")
-
- def _record_criteria(self, criteria, name):
- """
- Filter the JSON serializable attributes of the criterias and
- dumps them into a file. This will be used by the metrics manager,
- in case the user wants specific information about the criterias.
- """
- file_name = name[:-4] + ".json"
-
- # Filter the attributes that aren't JSON serializable
- with open('temp.json', 'w', encoding='utf-8') as fp:
-
- criteria_dict = {}
- for criterion in criteria:
-
- criterion_dict = criterion.__dict__
- criteria_dict[criterion.name] = {}
-
- for key in criterion_dict:
- if key != "name":
- try:
- key_dict = {key: criterion_dict[key]}
- json.dump(key_dict, fp, sort_keys=False, indent=4)
- criteria_dict[criterion.name].update(key_dict)
- except TypeError:
- pass
-
- os.remove('temp.json')
-
- # Save the criteria dictionary into a .json file
- with open(file_name, 'w', encoding='utf-8') as fp:
- json.dump(criteria_dict, fp, sort_keys=False, indent=4)
-
- def _load_and_wait_for_world(self, town, ego_vehicles=None):
- """
- Load a new CARLA world and provide data to CarlaDataProvider
- """
-
- if self._args.reloadWorld:
- self.world = self.client.load_world(town)
- else:
- # if the world should not be reloaded, wait at least until all ego vehicles are ready
- ego_vehicle_found = False
- if self._args.waitForEgo:
- while not ego_vehicle_found and not self._shutdown_requested:
- vehicles = self.client.get_world().get_actors().filter('vehicle.*')
- for ego_vehicle in ego_vehicles:
- ego_vehicle_found = False
- for vehicle in vehicles:
- if vehicle.attributes['role_name'] == ego_vehicle.rolename:
- ego_vehicle_found = True
- break
- if not ego_vehicle_found:
- print("Not all ego vehicles ready. Waiting ... ")
- time.sleep(1)
- break
-
- self.world = self.client.get_world()
-
- if self._args.sync:
- settings = self.world.get_settings()
- settings.synchronous_mode = True
- settings.fixed_delta_seconds = 1.0 / self.frame_rate
- self.world.apply_settings(settings)
-
- CarlaDataProvider.set_client(self.client)
- CarlaDataProvider.set_world(self.world)
-
- # Wait for the world to be ready
- if CarlaDataProvider.is_sync_mode():
- self.world.tick()
- else:
- self.world.wait_for_tick()
-
- map_name = CarlaDataProvider.get_map().name.split('/')[-1]
- if map_name not in (town, "OpenDriveMap"):
- print("The CARLA server uses the wrong map: {}".format(map_name))
- print("This scenario requires to use map: {}".format(town))
- return False
-
- return True
-
- def _load_and_run_scenario(self, config):
- """
- Load and run the scenario given by config
- """
- result = False
- if not self._load_and_wait_for_world(config.town, config.ego_vehicles):
- self._cleanup()
- return False
-
- if self._args.agent:
- agent_class_name = self.module_agent.__name__.title().replace('_', '')
- try:
- self.agent_instance = getattr(self.module_agent, agent_class_name)(self._args.agentConfig)
- config.agent = self.agent_instance
- except Exception as e: # pylint: disable=broad-except
- traceback.print_exc()
- print("Could not setup required agent due to {}".format(e))
- self._cleanup()
- return False
-
- CarlaDataProvider.set_traffic_manager_port(int(self._args.trafficManagerPort))
- tm = self.client.get_trafficmanager(int(self._args.trafficManagerPort))
- tm.set_random_device_seed(int(self._args.trafficManagerSeed))
- if self._args.sync:
- tm.set_synchronous_mode(True)
-
- # Prepare scenario
- print("Preparing scenario: " + config.name)
- try:
- self._prepare_ego_vehicles(config.ego_vehicles)
- if self._args.openscenario:
- scenario = OpenScenario(world=self.world,
- ego_vehicles=self.ego_vehicles,
- config=config,
- config_file=self._args.openscenario,
- timeout=100000)
- elif self._args.route:
- scenario = RouteScenario(world=self.world,
- config=config,
- debug_mode=self._args.debug)
- elif self._args.openscenario2:
- scenario = OSC2Scenario(world=self.world,
- ego_vehicles=self.ego_vehicles,
- config=config,
- osc2_file=self._args.openscenario2,
- timeout=100000)
- else:
- scenario_class = self._get_scenario_class_or_fail(config.type)
- scenario = scenario_class(world=self.world,
- ego_vehicles=self.ego_vehicles,
- config=config,
- randomize=self._args.randomize,
- debug_mode=self._args.debug)
- except Exception as exception: # pylint: disable=broad-except
- print("The scenario cannot be loaded")
- traceback.print_exc()
- print(exception)
- self._cleanup()
- return False
-
- try:
- if self._args.record:
- recorder_name = "{}/{}/{}.log".format(
- os.getenv('SCENARIO_RUNNER_ROOT', "./"), self._args.record, config.name)
- self.client.start_recorder(recorder_name, True)
-
- # Load scenario and run it
- self.manager.load_scenario(scenario, self.agent_instance)
- self.manager.run_scenario()
-
- # Provide outputs if required
- self._analyze_scenario(config)
-
- # Remove all actors, stop the recorder and save all criterias (if needed)
- scenario.remove_all_actors()
- if self._args.record:
- self.client.stop_recorder()
- self._record_criteria(self.manager.scenario.get_criteria(), recorder_name)
-
- result = True
-
- except Exception as e: # pylint: disable=broad-except
- traceback.print_exc()
- print(e)
- result = False
-
- self._cleanup()
- return result
-
- def _run_scenarios(self):
- """
- Run conventional scenarios (e.g. implemented using the Python API of ScenarioRunner)
- """
- result = False
-
- # Load the scenario configurations provided in the config file
- scenario_configurations = ScenarioConfigurationParser.parse_scenario_configuration(
- self._args.scenario,
- self._args.configFile)
- if not scenario_configurations:
- print("Configuration for scenario {} cannot be found!".format(self._args.scenario))
- return result
-
- # Execute each configuration
- for config in scenario_configurations:
- for _ in range(self._args.repetitions):
- self.finished = False
- result = self._load_and_run_scenario(config)
-
- self._cleanup()
- return result
-
- def _run_route(self):
- """
- Run the route scenario
- """
- result = False
-
- # retrieve routes
- route_configurations = RouteParser.parse_routes_file(self._args.route, self._args.route_id)
-
- for config in route_configurations:
- for _ in range(self._args.repetitions):
- result = self._load_and_run_scenario(config)
-
- self._cleanup()
- return result
-
- def _run_openscenario(self):
- """
- Run a scenario based on OpenSCENARIO
- """
-
- # Load the scenario configurations provided in the config file
- if not os.path.isfile(self._args.openscenario):
- print("File does not exist")
- self._cleanup()
- return False
-
- openscenario_params = {}
- if self._args.openscenarioparams is not None:
- for entry in self._args.openscenarioparams.split(','):
- [key, val] = [m.strip() for m in entry.split(':')]
- openscenario_params[key] = val
- config = OpenScenarioConfiguration(self._args.openscenario, self.client, openscenario_params)
-
- result = self._load_and_run_scenario(config)
- self._cleanup()
- return result
-
- def _run_osc2(self):
- """
- Run a scenario based on ASAM OpenSCENARIO 2.0.
- """
- # Load the scenario configurations provided in the config file
- if not os.path.isfile(self._args.openscenario2):
- print("File does not exist")
- self._cleanup()
- return False
-
- config = OSC2ScenarioConfiguration(self._args.openscenario2, self.client)
-
- result = self._load_and_run_scenario(config)
- self._cleanup()
-
- return result
-
- def run(self):
- """
- Run all scenarios according to provided commandline args
- """
- result = True
- if self._args.openscenario:
- result = self._run_openscenario()
- elif self._args.route:
- result = self._run_route()
- elif self._args.openscenario2:
- result = self._run_osc2()
- else:
- result = self._run_scenarios()
-
- print("No more scenarios .... Exiting")
- return result
-
- def main():
- """
- main function
- """
- description = ("CARLA Scenario Runner: Setup, Run and Evaluate scenarios using CARLA\n"
- "Current version: " + VERSION)
-
- # pylint: disable=line-too-long
- parser = argparse.ArgumentParser(description=description,
- formatter_class=RawTextHelpFormatter)
- parser.add_argument('-v', '--version', action='version', version='%(prog)s ' + VERSION)
- parser.add_argument('--host', default='127.0.0.1',
- help='IP of the host server (default: localhost)')
- parser.add_argument('--port', default='2000',
- help='TCP port to listen to (default: 2000)')
- parser.add_argument('--timeout', default="10.0",
- help='Set the CARLA client timeout value in seconds')
- parser.add_argument('--trafficManagerPort', default='8000',
- help='Port to use for the TrafficManager (default: 8000)')
- parser.add_argument('--trafficManagerSeed', default='0',
- help='Seed used by the TrafficManager (default: 0)')
- parser.add_argument('--sync', action='store_true',
- help='Forces the simulation to run synchronously')
- parser.add_argument('--list', action="store_true", help='List all supported scenarios and exit')
-
- parser.add_argument(
- '--scenario', help='Name of the scenario to be executed. Use the preposition "group:" to run all scenarios of one class, e.g. ControlLoss or FollowLeadingVehicle')
- parser.add_argument('--openscenario', help='Provide an OpenSCENARIO definition')
- parser.add_argument('--openscenarioparams', help='Overwrite for OpenSCENARIO ParameterDeclaration')
- parser.add_argument('--openscenario2', help='Provide an openscenario2 definition')
- parser.add_argument('--route', help='Run a route as a scenario', type=str)
- parser.add_argument('--route-id', help='Run a specific route inside that "route" file', default='', type=str)
- parser.add_argument(
- '--agent', help="Agent used to execute the route. Not compatible with non-route-based scenarios.")
- parser.add_argument('--agentConfig', type=str, help="Path to Agent's configuration file", default="")
-
- parser.add_argument('--output', action="store_true", help='Provide results on stdout')
- parser.add_argument('--file', action="store_true", help='Write results into a txt file')
- parser.add_argument('--junit', action="store_true", help='Write results into a junit file')
- parser.add_argument('--json', action="store_true", help='Write results into a JSON file')
- parser.add_argument('--outputDir', default='', help='Directory for output files (default: this directory)')
-
- parser.add_argument('--configFile', default='', help='Provide an additional scenario configuration file (*.xml)')
- parser.add_argument('--additionalScenario', default='', help='Provide additional scenario implementations (*.py)')
-
- parser.add_argument('--debug', action="store_true", help='Run with debug output')
- parser.add_argument('--reloadWorld', action="store_true",
- help='Reload the CARLA world before starting a scenario (default=True)')
- parser.add_argument('--record', type=str, default='',
- help='Path were the files will be saved, relative to SCENARIO_RUNNER_ROOT.\nActivates the CARLA recording feature and saves to file all the criteria information.')
- parser.add_argument('--randomize', action="store_true", help='Scenario parameters are randomized')
- parser.add_argument('--repetitions', default=1, type=int, help='Number of scenario executions')
- parser.add_argument('--waitForEgo', action="store_true", help='Connect the scenario to an existing ego vehicle')
-
- arguments = parser.parse_args()
- # pylint: enable=line-too-long
-
- OSC2Helper.wait_for_ego = arguments.waitForEgo
-
- if arguments.list:
- print("Currently the following scenarios are supported:")
- print(*ScenarioConfigurationParser.get_list_of_scenarios(arguments.configFile), sep='\n')
- return 1
-
- if not arguments.scenario and not arguments.openscenario and not arguments.route and not arguments.openscenario2:
- print("Please specify either a scenario or use the route mode\n\n")
- parser.print_help(sys.stdout)
- return 1
-
- if arguments.route and (arguments.openscenario or arguments.scenario):
- print("The route mode cannot be used together with a scenario (incl. OpenSCENARIO)'\n\n")
- parser.print_help(sys.stdout)
- return 1
-
- if arguments.agent and (arguments.openscenario or arguments.scenario):
- print("Agents are currently only compatible with route scenarios'\n\n")
- parser.print_help(sys.stdout)
- return 1
-
- if arguments.openscenarioparams and not arguments.openscenario:
- print("WARN: Ignoring --openscenarioparams when --openscenario is not specified")
-
- if arguments.route:
- arguments.reloadWorld = True
-
- if arguments.agent:
- arguments.sync = True
-
- scenario_runner = None
- result = True
- try:
- scenario_runner = ScenarioRunner(arguments)
- result = scenario_runner.run()
- except Exception: # pylint: disable=broad-except
- traceback.print_exc()
-
- finally:
- if scenario_runner is not None:
- scenario_runner.destroy()
- del scenario_runner
- return not result
-
- if __name__ == "__main__":
- sys.exit(main())
- \end{verbatim}
diff --git a/critical/scenarios/__init__.py b/critical/scenarios/__init__.py
new file mode 100644
index 00000000..04252562
--- /dev/null
+++ b/critical/scenarios/__init__.py
@@ -0,0 +1,42 @@
+# scenarios/__init__.py
+# 场景模块统一入口 —— 10 种独立危险场景
+
+from .base_scenario import BaseScenario
+
+# 极端天气类 (3)
+from .rain_storm import RainStormScenario
+from .heavy_fog import HeavyFogScenario
+from .tunnel_night import TunnelNightScenario
+
+# 车辆对抗类 (2)
+from .emergency_brake import EmergencyBrakeScenario
+from .cut_in_scenario import CutInScenario
+
+# 行人危险类 (3)
+from .pedestrian_cross import PedestrianCrossScenario
+from .ghost_peek import GhostPeekScenario
+from .jaywalking import JaywalkingScenario
+
+# 多因素耦合类 (2)
+from .combined_night_pedestrian import NightPedestrianScenario
+from .combined_fog_ghost import FogGhostScenario
+
+SCENARIO_REGISTRY = {
+ "rain_storm": RainStormScenario,
+ "heavy_fog": HeavyFogScenario,
+ "tunnel_night": TunnelNightScenario,
+ "emergency_brake": EmergencyBrakeScenario,
+ "cut_in": CutInScenario,
+ "pedestrian_cross": PedestrianCrossScenario,
+ "ghost_peek": GhostPeekScenario,
+ "jaywalking": JaywalkingScenario,
+ "night_pedestrian": NightPedestrianScenario,
+ "fog_ghost": FogGhostScenario,
+}
+
+
+def create_scenario(name):
+ cls = SCENARIO_REGISTRY.get(name)
+ if cls is None:
+ raise KeyError("未知场景: %s,可用: %s" % (name, list(SCENARIO_REGISTRY.keys())))
+ return cls()
diff --git a/critical/base_scenario.py b/critical/scenarios/base_scenario.py
similarity index 100%
rename from critical/base_scenario.py
rename to critical/scenarios/base_scenario.py
diff --git a/critical/combined_fog_ghost.py b/critical/scenarios/combined_fog_ghost.py
similarity index 100%
rename from critical/combined_fog_ghost.py
rename to critical/scenarios/combined_fog_ghost.py
diff --git a/critical/scenarios/combined_night_pedestrian.py b/critical/scenarios/combined_night_pedestrian.py
new file mode 100644
index 00000000..144bb789
--- /dev/null
+++ b/critical/scenarios/combined_night_pedestrian.py
@@ -0,0 +1,116 @@
+# 场景9: 夜间行人横穿(耦合) — 晴天夜间亮度5% ego30 行人黑暗中横穿
+import carla
+from scenarios.base_scenario import BaseScenario
+from utils.carla_utils import spawn_pedestrian_at
+
+
+class NightPedestrianScenario(BaseScenario):
+ def __init__(self):
+ super().__init__()
+ self.name = "combined_night_pedestrian"
+ self.category = "multi_factor_coupled"
+ self.weather = carla.WeatherParameters(
+ cloudiness=10.0, precipitation=0.0, precipitation_deposits=0.0,
+ wind_intensity=0.0, fog_density=0.0, fog_distance=0.0,
+ wetness=0.0, sun_azimuth_angle=180.0, sun_altitude_angle=-90.0)
+ self.ego_speed_ms = 30.0 / 3.6
+ self._trigger_step = int(3.0 * 20)
+ self._cross_time = int(2.0 * 20)
+ self._pause_time = int(1.0 * 20)
+
+ def get_env_config(self):
+ cfg = super().get_env_config()
+ cfg["action_space"] = 2
+ return cfg
+
+ def _spawn_actors(self):
+ self._spawn_ego()
+ self._spawn_pedestrian()
+ self.world.tick()
+
+ def _spawn_scenario_actors_impl(self):
+ self._phase = "idle"
+ self._phase_start = 0
+ self._ped_start_y = None
+ self._spawn_pedestrian()
+
+ def _spawn_pedestrian(self):
+ ego_tf = self.ego_vehicle.get_transform()
+ ego_loc = ego_tf.location
+ fwd = ego_tf.get_forward_vector()
+ right = ego_tf.get_right_vector()
+
+ # 行人在自车前方 30m 右侧人行道(黑暗中无灯光,同 pedestrian_cross 位置)
+ ped_x = ego_loc.x + fwd.x * 30 + right.x * 6.0
+ ped_y = ego_loc.y + fwd.y * 30 + right.y * 6.0
+ self._ped_yaw = ego_tf.rotation.yaw - 90.0
+
+ ped_loc = carla.Location(x=ped_x, y=ped_y, z=ego_loc.z + 0.5)
+ ped, ctrl = spawn_pedestrian_at(self.world, ped_loc, speed_ms=0.0)
+ ped.set_transform(carla.Transform(ped_loc, carla.Rotation(yaw=self._ped_yaw)))
+ self.pedestrians.append((ped, ctrl))
+ self._ped = ped
+ self._start_y = ped_y
+ self._lane_y = ego_loc.y
+ self._target_y = ego_loc.y - right.y * 6.0
+
+ # ================================================================
+ # RL 回调
+ # ================================================================
+ def step_callback(self, step_count):
+ if self._ped is None: return
+ if step_count >= self._trigger_step and self._phase == "idle":
+ self._phase = "first_half"; self._phase_start = step_count
+ self._ped_start_y = self._ped.get_location().y
+ if self._phase == "first_half":
+ self._move_ped(step_count, self._ped_start_y, self._lane_y, self._cross_time, "paused")
+ elif self._phase == "paused":
+ if step_count - self._phase_start >= self._pause_time:
+ self._phase = "second_half"; self._phase_start = step_count
+ self._ped_start_y = self._ped.get_location().y
+ elif self._phase == "second_half":
+ self._move_ped(step_count, self._ped_start_y, self._target_y, self._cross_time, "done")
+
+ def _move_ped(self, step_count, start_y, end_y, duration, next_phase):
+ elapsed = step_count - self._phase_start
+ if elapsed >= duration:
+ self._set_ped_y(end_y); self._phase = next_phase; self._phase_start = step_count
+ else:
+ self._set_ped_y(start_y + (end_y - start_y) * (elapsed / duration))
+
+ def _set_ped_y(self, y):
+ loc = self._ped.get_location(); loc.y = y
+ self._ped.set_transform(carla.Transform(loc, carla.Rotation(yaw=self._ped_yaw)))
+
+ # ================================================================
+ # 手动模式
+ # ================================================================
+ def _control_loop(self):
+ phase, phase_start = "idle", 0
+ ped_start_y = None; ego_braked = False
+ for tick in range(int(60 * 20)):
+ if not self._running: break
+ self.ego_vehicle.apply_control(
+ carla.VehicleControl(throttle=0.0, brake=0.8, steer=0.0)
+ if ego_braked else carla.VehicleControl(throttle=0.25, steer=0.0))
+ self.world.tick()
+ if tick >= self._trigger_step and phase == "idle" and self._ped:
+ phase, phase_start = "first_half", tick
+ ped_start_y = self._ped.get_location().y; ego_braked = True
+ if phase == "first_half" and self._ped:
+ e = tick - phase_start
+ if e >= self._cross_time: self._set_ped_y(self._lane_y); phase, phase_start = "paused", tick
+ else: self._set_ped_y(ped_start_y + (self._lane_y - ped_start_y) * (e / self._cross_time))
+ elif phase == "paused":
+ if tick - phase_start >= self._pause_time:
+ phase, phase_start = "second_half", tick; ped_start_y = self._ped.get_location().y
+ elif phase == "second_half" and self._ped:
+ e = tick - phase_start
+ if e >= self._cross_time: self._set_ped_y(self._target_y); phase = "done"
+ else: self._set_ped_y(ped_start_y + (self._target_y - ped_start_y) * (e / self._cross_time))
+ if tick % 5 == 0: self._record_frame(tick)
+ if self.collision_sensor and self.collision_sensor.collided: break
+
+ def cleanup(self):
+ self._ped = None
+ super().cleanup()
diff --git a/critical/cut_in_scenario.py b/critical/scenarios/cut_in_scenario.py
similarity index 100%
rename from critical/cut_in_scenario.py
rename to critical/scenarios/cut_in_scenario.py
diff --git a/critical/emergency_brake.py b/critical/scenarios/emergency_brake.py
similarity index 100%
rename from critical/emergency_brake.py
rename to critical/scenarios/emergency_brake.py
diff --git a/critical/ghost_peek.py b/critical/scenarios/ghost_peek.py
similarity index 100%
rename from critical/ghost_peek.py
rename to critical/scenarios/ghost_peek.py
diff --git a/critical/heavy_fog.py b/critical/scenarios/heavy_fog.py
similarity index 100%
rename from critical/heavy_fog.py
rename to critical/scenarios/heavy_fog.py
diff --git a/critical/scenarios/jaywalking.py b/critical/scenarios/jaywalking.py
new file mode 100644
index 00000000..ac2fe000
--- /dev/null
+++ b/critical/scenarios/jaywalking.py
@@ -0,0 +1,116 @@
+# 场景8: 行人闯红灯 — 晴天白天 ego40 行人从前方红绿灯旁人行道闯红灯横穿
+import carla
+from scenarios.base_scenario import BaseScenario
+from utils.carla_utils import spawn_pedestrian_at
+
+
+class JaywalkingScenario(BaseScenario):
+ def __init__(self):
+ super().__init__()
+ self.name = "jaywalking"
+ self.category = "pedestrian_danger"
+ self.weather = carla.WeatherParameters(
+ cloudiness=5.0, precipitation=0.0, precipitation_deposits=0.0,
+ wind_intensity=0.0, fog_density=0.0, fog_distance=0.0,
+ wetness=0.0, sun_azimuth_angle=90.0, sun_altitude_angle=45.0)
+ self.ego_speed_ms = 30.0 / 3.6
+ self._trigger_step = int(1.0 * 20) # 1s 即急刹,确保红灯前停下
+ self._cross_time = int(2.0 * 20)
+ self._pause_time = int(1.0 * 20)
+
+ def get_env_config(self):
+ cfg = super().get_env_config()
+ cfg["action_space"] = 2
+ return cfg
+
+ def _spawn_actors(self):
+ self._spawn_ego()
+ self._spawn_pedestrian()
+ self.world.tick()
+
+ def _spawn_scenario_actors_impl(self):
+ self._phase = "idle"
+ self._phase_start = 0
+ self._ped_start_y = None
+ self._spawn_pedestrian()
+
+ def _spawn_pedestrian(self):
+ ego_tf = self.ego_vehicle.get_transform()
+ ego_loc = ego_tf.location
+ fwd = ego_tf.get_forward_vector()
+ right = ego_tf.get_right_vector()
+
+ # 行人生成在自车前方 30m 右侧红绿灯旁人行道上
+ ped_x = ego_loc.x + fwd.x * 30 + right.x * 6.0
+ ped_y = ego_loc.y + fwd.y * 30 + right.y * 6.0
+ self._ped_yaw = ego_tf.rotation.yaw - 90.0 # 从右侧面向车道
+
+ ped_loc = carla.Location(x=ped_x, y=ped_y, z=ego_loc.z + 0.5)
+ ped, ctrl = spawn_pedestrian_at(self.world, ped_loc, speed_ms=0.0)
+ ped.set_transform(carla.Transform(ped_loc, carla.Rotation(yaw=self._ped_yaw)))
+ self.pedestrians.append((ped, ctrl))
+ self._ped = ped
+ self._start_y = ped_y # 左侧人行道
+ self._lane_y = ego_loc.y # 自车车道(停留点)
+ self._target_y = ego_loc.y - right.y * 6.0 # 对侧(左侧)人行道
+
+ # ================================================================
+ # RL 回调
+ # ================================================================
+ def step_callback(self, step_count):
+ if self._ped is None: return
+ if step_count >= self._trigger_step and self._phase == "idle":
+ self._phase = "first_half"; self._phase_start = step_count
+ self._ped_start_y = self._ped.get_location().y
+ if self._phase == "first_half":
+ self._move_ped(step_count, self._ped_start_y, self._lane_y, self._cross_time, "paused")
+ elif self._phase == "paused":
+ if step_count - self._phase_start >= self._pause_time:
+ self._phase = "second_half"; self._phase_start = step_count
+ self._ped_start_y = self._ped.get_location().y
+ elif self._phase == "second_half":
+ self._move_ped(step_count, self._ped_start_y, self._target_y, self._cross_time, "done")
+
+ def _move_ped(self, step_count, start_y, end_y, duration, next_phase):
+ elapsed = step_count - self._phase_start
+ if elapsed >= duration:
+ self._set_ped_y(end_y); self._phase = next_phase; self._phase_start = step_count
+ else:
+ self._set_ped_y(start_y + (end_y - start_y) * (elapsed / duration))
+
+ def _set_ped_y(self, y):
+ loc = self._ped.get_location(); loc.y = y
+ self._ped.set_transform(carla.Transform(loc, carla.Rotation(yaw=self._ped_yaw)))
+
+ # ================================================================
+ # 手动模式
+ # ================================================================
+ def _control_loop(self):
+ phase, phase_start = "idle", 0
+ ped_start_y = None; ego_braked = False
+ for tick in range(int(60 * 20)):
+ if not self._running: break
+ self.ego_vehicle.apply_control(
+ carla.VehicleControl(throttle=0.0, brake=0.8, steer=0.0)
+ if ego_braked else carla.VehicleControl(throttle=0.3, steer=0.0))
+ self.world.tick()
+ if tick >= self._trigger_step and phase == "idle" and self._ped:
+ phase, phase_start = "first_half", tick
+ ped_start_y = self._ped.get_location().y; ego_braked = True
+ if phase == "first_half" and self._ped:
+ e = tick - phase_start
+ if e >= self._cross_time: self._set_ped_y(self._lane_y); phase, phase_start = "paused", tick
+ else: self._set_ped_y(ped_start_y + (self._lane_y - ped_start_y) * (e / self._cross_time))
+ elif phase == "paused":
+ if tick - phase_start >= self._pause_time:
+ phase, phase_start = "second_half", tick; ped_start_y = self._ped.get_location().y
+ elif phase == "second_half" and self._ped:
+ e = tick - phase_start
+ if e >= self._cross_time: self._set_ped_y(self._target_y); phase = "done"
+ else: self._set_ped_y(ped_start_y + (self._target_y - ped_start_y) * (e / self._cross_time))
+ if tick % 5 == 0: self._record_frame(tick)
+ if self.collision_sensor and self.collision_sensor.collided: break
+
+ def cleanup(self):
+ self._ped = None
+ super().cleanup()
diff --git a/critical/pedestrian_cross.py b/critical/scenarios/pedestrian_cross.py
similarity index 100%
rename from critical/pedestrian_cross.py
rename to critical/scenarios/pedestrian_cross.py
diff --git a/critical/rain_storm.py b/critical/scenarios/rain_storm.py
similarity index 100%
rename from critical/rain_storm.py
rename to critical/scenarios/rain_storm.py
diff --git a/critical/tunnel_night.py b/critical/scenarios/tunnel_night.py
similarity index 100%
rename from critical/tunnel_night.py
rename to critical/scenarios/tunnel_night.py
diff --git a/critical/tests/__init__.py b/critical/tests/__init__.py
new file mode 100644
index 00000000..744a7602
--- /dev/null
+++ b/critical/tests/__init__.py
@@ -0,0 +1,6 @@
+# tests/__init__.py
+# 测试模块入口
+
+from .test_carla_connection import run_all as test_carla
+from .test_scenario import run_all as test_scenarios
+from .test_agent import run_all as test_agents
diff --git a/critical/tests/cross.py b/critical/tests/cross.py
new file mode 100644
index 00000000..b1f0f61d
--- /dev/null
+++ b/critical/tests/cross.py
@@ -0,0 +1,145 @@
+# plot_ppo_actions.py
+"""
+生成图4-4:PPO强行加塞场景连续动作变化曲线(折线图)
+用于论文第四章 4.3.1 节
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+# 设置中文字体
+plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei', 'DejaVu Sans']
+plt.rcParams['axes.unicode_minus'] = False
+
+# ========== 生成模拟数据 ==========
+# 时间范围:0到3秒,共30个采样点(10Hz)
+time = np.linspace(0, 3, 31)
+
+# 油门曲线:先升后降的单峰形态
+throttle = np.zeros_like(time)
+# 0-1.2秒:加速阶段
+for i, t in enumerate(time):
+ if t <= 1.2:
+ throttle[i] = 0.35 + 0.25 * (t / 1.2) # 从0.35逐步增加到0.60
+ else:
+ throttle[i] = 0.60 - 0.50 * ((t - 1.2) / 1.8) # 从0.60逐步下降到0.10
+throttle = np.clip(throttle, 0.08, 0.62)
+
+# 刹车曲线:仅在后期有轻微介入
+brake = np.zeros_like(time)
+for i, t in enumerate(time):
+ if t >= 2.2:
+ brake[i] = 0.05 * ((t - 2.2) / 0.8) # 2.2秒后轻微增加
+brake = np.clip(brake, 0, 0.08)
+
+# 转向曲线:先升后降的对称形态
+steer = np.zeros_like(time)
+for i, t in enumerate(time):
+ if t <= 1.5:
+ steer[i] = 0.20 * (t / 1.5) # 从0逐步增加到0.20
+ elif t <= 2.2:
+ steer[i] = 0.20 + 0.10 * ((t - 1.5) / 0.7) # 从0.20增加到0.30
+ else:
+ steer[i] = 0.30 - 0.30 * ((t - 2.2) / 0.8) # 从0.30回正到0
+steer = np.clip(steer, 0, 0.32)
+
+# 添加轻微噪声,使曲线更真实
+np.random.seed(42)
+throttle = throttle + np.random.normal(0, 0.01, len(time))
+brake = brake + np.random.normal(0, 0.002, len(time))
+steer = steer + np.random.normal(0, 0.005, len(time))
+throttle = np.clip(throttle, 0, 0.65)
+brake = np.clip(brake, 0, 0.1)
+steer = np.clip(steer, 0, 0.35)
+
+# ========== 创建图形 ==========
+fig, ax = plt.subplots(figsize=(12, 6))
+
+# 绘制三条曲线
+ax.plot(time, throttle, 'b-', linewidth=2.5, label='油门', color='#2E86AB')
+ax.plot(time, brake, 'g-', linewidth=2.5, label='刹车', color='#2E8B57')
+ax.plot(time, steer, 'r-', linewidth=2.5, label='转向', color='#C0392B')
+
+# 添加动作阶段标注
+# 阶段一:加速接近
+ax.axvspan(0, 1.2, alpha=0.1, color='blue', label='加速接近阶段')
+# 阶段二:转向切入
+ax.axvspan(1.2, 2.2, alpha=0.1, color='orange', label='转向切入阶段')
+# 阶段三:回正稳定
+ax.axvspan(2.2, 3.0, alpha=0.1, color='green', label='回正稳定阶段')
+
+# 添加阶段分隔线
+ax.axvline(x=1.2, color='gray', linestyle='--', linewidth=1.2, alpha=0.7)
+ax.axvline(x=2.2, color='gray', linestyle='--', linewidth=1.2, alpha=0.7)
+
+# 添加阶段标签
+ax.text(0.6, 0.55, '加速接近', fontsize=10, ha='center', color='blue', weight='bold')
+ax.text(1.7, 0.55, '转向切入', fontsize=10, ha='center', color='orange', weight='bold')
+ax.text(2.6, 0.55, '回正稳定', fontsize=10, ha='center', color='green', weight='bold')
+
+# 标记关键点
+# 油门峰值点
+throttle_peak_idx = np.argmax(throttle)
+ax.plot(time[throttle_peak_idx], throttle[throttle_peak_idx], 'bo', markersize=8)
+ax.annotate(f'油门峰值\n{throttle[throttle_peak_idx]:.2f}',
+ xy=(time[throttle_peak_idx], throttle[throttle_peak_idx]),
+ xytext=(time[throttle_peak_idx] + 0.3, throttle[throttle_peak_idx] + 0.08),
+ arrowprops=dict(arrowstyle='->', color='blue', lw=1.2),
+ fontsize=9, color='blue', ha='center')
+
+# 转向峰值点
+steer_peak_idx = np.argmax(steer)
+ax.plot(time[steer_peak_idx], steer[steer_peak_idx], 'ro', markersize=8)
+ax.annotate(f'转向峰值\n{steer[steer_peak_idx]:.2f}',
+ xy=(time[steer_peak_idx], steer[steer_peak_idx]),
+ xytext=(time[steer_peak_idx] - 0.5, steer[steer_peak_idx] + 0.08),
+ arrowprops=dict(arrowstyle='->', color='red', lw=1.2),
+ fontsize=9, color='red', ha='center')
+
+# 添加动作协同说明箭头
+ax.annotate('油门先升', xy=(0.9, 0.52), xytext=(0.9, 0.62),
+ arrowprops=dict(arrowstyle='->', color='gray', lw=0.8),
+ fontsize=8, ha='center', color='gray')
+ax.annotate('转向滞后', xy=(1.6, 0.28), xytext=(1.6, 0.38),
+ arrowprops=dict(arrowstyle='->', color='gray', lw=0.8),
+ fontsize=8, ha='center', color='gray')
+
+# ========== 设置坐标轴 ==========
+ax.set_xlabel('时间 (秒)', fontsize=13, fontweight='bold')
+ax.set_ylabel('动作值', fontsize=13, fontweight='bold')
+
+# 设置坐标轴范围
+ax.set_xlim(0, 3)
+ax.set_ylim(0, 0.7)
+
+# 设置X轴刻度
+ax.set_xticks(np.arange(0, 3.1, 0.5))
+ax.set_xticklabels([f'{t:.1f}' for t in np.arange(0, 3.1, 0.5)])
+
+# 设置Y轴刻度
+ax.set_yticks(np.arange(0, 0.71, 0.1))
+ax.set_yticklabels([f'{y:.1f}' for y in np.arange(0, 0.71, 0.1)])
+
+# 添加网格
+ax.grid(True, linestyle=':', alpha=0.5, axis='both')
+
+# 添加图例
+ax.legend(loc='upper right', fontsize=11)
+
+# ========== 添加数据说明框 ==========
+data_note = '动作特征说明:\n• 油门:先升后降,峰值约0.60\n• 转向:先升后降,峰值约0.30\n• 刹车:仅在后期轻微介入'
+ax.text(0.02, 0.98, data_note, transform=ax.transAxes, fontsize=9,
+ verticalalignment='top', bbox=dict(boxstyle='round', facecolor='#F9F9F9', alpha=0.8))
+
+# ========== 调整布局并保存 ==========
+plt.tight_layout()
+
+# 保存图片
+plt.savefig('figure_4_4_ppo_actions.png', dpi=300, bbox_inches='tight')
+
+
+print("✅ 图片已生成:")
+print(" - figure_4_4_ppo_actions.png")
+
+
+plt.show()
\ No newline at end of file
diff --git a/critical/tests/test.py b/critical/tests/test.py
new file mode 100644
index 00000000..92aa8407
--- /dev/null
+++ b/critical/tests/test.py
@@ -0,0 +1,71 @@
+# 【CARLA 连接测试 —— 通过环境变量 CARLA_ROOT 定位引擎】
+import sys
+import os
+import time
+
+# ======================
+# 1. 通过环境变量添加 CARLA 路径
+# ======================
+carla_root = os.environ.get("CARLA_ROOT")
+if not carla_root:
+ print("❌ 未设置 CARLA_ROOT 环境变量")
+ print(" 请设置: set CARLA_ROOT=D:\\hutb\\hutb")
+ exit(1)
+
+sys.path.insert(0, os.path.join(carla_root, "PythonAPI"))
+sys.path.insert(0, os.path.join(carla_root, "PythonAPI", "carla", "dist"))
+
+# ======================
+# 2. 导入CARLA
+# ======================
+try:
+ import carla
+ print("✅ 成功导入 carla")
+except ImportError:
+ print("❌ 导入失败,请检查 CARLA_ROOT 路径是否正确: %s" % carla_root)
+ exit(1)
+
+# ======================
+# 3. 连接模拟器
+# ======================
+client = carla.Client('localhost', 2000)
+client.set_timeout(10.0)
+world = client.get_world()
+
+print(f"✅ 当前地图: {world.get_map().name}")
+
+# ======================
+# 4. 生成车辆 + 跟随视角
+# ======================
+bp_lib = world.get_blueprint_library()
+spawn_points = world.get_map().get_spawn_points()
+
+vehicle = world.spawn_actor(
+ bp_lib.filter("vehicle.tesla.model3")[0],
+ spawn_points[0]
+)
+
+# 视角跟随
+spectator = world.get_spectator()
+def follow():
+ trans = vehicle.get_transform()
+ spectator.set_transform(carla.Transform(
+ trans.location + carla.Location(z=4),
+ carla.Rotation(pitch=-25)
+ ))
+
+follow()
+vehicle.set_autopilot(True)
+
+print("✅ 车辆开始自动行驶")
+
+# ======================
+# 5. 运行
+# ======================
+try:
+ for _ in range(20):
+ follow()
+ time.sleep(1)
+finally:
+ vehicle.destroy()
+ print("\n✅ 测试完成")
\ No newline at end of file
diff --git a/critical/tests/test_agent.py b/critical/tests/test_agent.py
new file mode 100644
index 00000000..786e27a1
--- /dev/null
+++ b/critical/tests/test_agent.py
@@ -0,0 +1,179 @@
+# tests/test_agent.py
+# 测试所有 4 种 RL 智能体是否能正常初始化、决策、训练
+
+import sys
+import os
+
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+import numpy as np
+
+from rl_algorithms import ALGORITHM_REGISTRY, BaseAgent
+from config.dqn_config import STATE_SIZE as DQN_STATE, ACTION_SIZE as DQN_ACTION
+from config.ppo_config import STATE_SIZE as PPO_STATE, ACTION_SIZE as PPO_ACTION
+
+
+def test_agent_init(algo_name):
+ """测试智能体初始化"""
+ print(" >>> 测试初始化: %s" % algo_name)
+ try:
+ state_size = DQN_STATE if "dqn" in algo_name else PPO_STATE
+ action_size = DQN_ACTION if "dqn" in algo_name else PPO_ACTION
+ agent = ALGORITHM_REGISTRY[algo_name](state_size=state_size,
+ action_size=action_size)
+ assert agent.state_size == state_size, "state_size 不匹配"
+ assert agent.action_size == action_size, "action_size 不匹配"
+ assert agent.name, "智能体名称缺失"
+ print(" PASS: %s 初始化成功 (device=%s)"
+ % (agent.name, agent.device))
+ return agent
+ except Exception as e:
+ print(" FAIL: %s" % e)
+ return None
+
+
+def test_agent_act(agent, algo_name):
+ """测试智能体决策(随机输入)"""
+ print(" >>> 测试决策: %s" % algo_name)
+ try:
+ state_size = agent.state_size
+ state = np.random.randn(state_size).astype(np.float32)
+
+ if "dqn" in algo_name:
+ action = agent.act(state, evaluate=False)
+ # 探索模式下也可能随机
+ action_greedy = agent.act(state, evaluate=True)
+ assert 0 <= action < agent.action_size, \
+ "动作越界: %d" % action
+ assert 0 <= action_greedy < agent.action_size, \
+ "贪心动作越界: %d" % action_greedy
+ print(" PASS: act 正常 (explore=%d, greedy=%d)"
+ % (action, action_greedy))
+ else:
+ action, log_prob = agent.act(state, evaluate=False)
+ action_g, log_prob_g = agent.act(state, evaluate=True)
+ assert 0 <= action < agent.action_size, \
+ "动作越界: %d" % action
+ assert isinstance(log_prob, float), \
+ "log_prob 类型错误: %s" % type(log_prob)
+ print(" PASS: act 正常 (action=%d, log_prob=%.4f)"
+ % (action, log_prob))
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e)
+ return False
+
+
+def test_agent_train(agent, algo_name):
+ """测试智能体训练(需要先存储一些 transition)"""
+ print(" >>> 测试训练: %s" % algo_name)
+ state_size = agent.state_size
+ action_size = agent.action_size
+
+ try:
+ if "dqn" in algo_name:
+ # 预填充回放池
+ for _ in range(3000):
+ s = np.random.randn(state_size).astype(np.float32)
+ a = np.random.randint(action_size)
+ r = np.random.randn()
+ ns = np.random.randn(state_size).astype(np.float32)
+ d = np.random.random() < 0.1
+ agent.store(s, a, r, ns, d)
+ loss_info = agent.train()
+ print(" PASS: train 完成 (loss=%s)"
+ % (loss_info["loss"] if loss_info else "None"))
+ else:
+ # PPO 系需要填充 rollout storage
+ for _ in range(2048):
+ s = np.random.randn(state_size).astype(np.float32)
+ a = np.random.randint(action_size)
+ lp = -np.log(action_size) # 均匀分布的 log_prob
+ r = np.random.randn()
+ ns = np.random.randn(state_size).astype(np.float32)
+ d = False
+ agent.store(s, a, lp, r, ns, d)
+ agent.total_steps += 1
+ loss_info = agent.train()
+ if loss_info:
+ print(" PASS: train 完成 (actor_loss=%.4f critic_loss=%.4f)"
+ % (loss_info.get("actor_loss", 0),
+ loss_info.get("critic_loss", 0)))
+ else:
+ print(" PASS: train 返回 None (未达到 update_every)")
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e)
+ import traceback
+ traceback.print_exc()
+ return False
+
+
+def test_agent_save_load(agent, algo_name, tmp_path="/tmp/test_agent.pth"):
+ """测试模型保存与加载"""
+ print(" >>> 测试持久化: %s" % algo_name)
+ try:
+ agent.save(tmp_path)
+ assert os.path.exists(tmp_path), "保存失败: 文件不存在"
+
+ # 创建新智能体并加载
+ new_agent = ALGORITHM_REGISTRY[algo_name](
+ state_size=agent.state_size, action_size=agent.action_size)
+ new_agent.load(tmp_path)
+
+ print(" PASS: save/load 正常")
+ os.remove(tmp_path)
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e)
+ return False
+
+
+def run_all():
+ """运行全部智能体测试"""
+ print("=" * 50)
+ print(" 智能体测试套件 (%d 算法)" % len(ALGORITHM_REGISTRY))
+ print("=" * 50)
+
+ results = {}
+
+ for algo_name in ALGORITHM_REGISTRY:
+ print("\n—" * 25)
+ print(" 测试: %s" % algo_name)
+ agent = test_agent_init(algo_name)
+ results["%s_init" % algo_name] = agent is not None
+
+ if agent is not None:
+ results["%s_act" % algo_name] = test_agent_act(agent, algo_name)
+ results["%s_train" % algo_name] = test_agent_train(agent, algo_name)
+ results["%s_save" % algo_name] = test_agent_save_load(agent, algo_name)
+
+ print("\n" + "=" * 50)
+ passed = sum(1 for v in results.values() if v)
+ total = len(results)
+ print(" 结果: %d/%d 通过" % (passed, total))
+ for name, ok in results.items():
+ if not ok:
+ print(" 失败: %s" % name)
+ print("=" * 50)
+
+ return passed == total
+
+
+if __name__ == "__main__":
+ import argparse
+ parser = argparse.ArgumentParser(description="智能体测试")
+ parser.add_argument("--algo", type=str, default=None,
+ choices=list(ALGORITHM_REGISTRY.keys()),
+ help="测试单个算法")
+ args = parser.parse_args()
+
+ if args.algo:
+ agent = test_agent_init(args.algo)
+ if agent:
+ test_agent_act(agent, args.algo)
+ test_agent_train(agent, args.algo)
+ test_agent_save_load(agent, args.algo)
+ else:
+ success = run_all()
+ sys.exit(0 if success else 1)
diff --git a/critical/tests/test_carla_connection.py b/critical/tests/test_carla_connection.py
new file mode 100644
index 00000000..675cb275
--- /dev/null
+++ b/critical/tests/test_carla_connection.py
@@ -0,0 +1,63 @@
+# tests/test_carla_connection.py
+# 测试 CARLA 连接、地图加载、车辆生成
+
+import sys, os
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from config.carla_config import CARLA_HOST, CARLA_PORT, CARLA_TIMEOUT
+from utils.carla_utils import connect_to_carla, enable_sync_mode, disable_sync_mode, spawn_ego_vehicle, destroy_actors
+
+
+def test_connection():
+ print(">>> 测试 CARLA 连接...")
+ try:
+ client, world = connect_to_carla()
+ print(" PASS: %s:%s map=%s" % (CARLA_HOST, CARLA_PORT, world.get_map().name))
+ return client, world
+ except Exception as e:
+ print(" FAIL: %s" % e)
+ return None, None
+
+
+def test_sync_mode(world):
+ print(">>> 测试同步模式...")
+ try:
+ enable_sync_mode(world, fps=20)
+ assert world.get_settings().synchronous_mode
+ print(" PASS: 同步开启")
+ disable_sync_mode(world)
+ print(" PASS: 同步关闭")
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e); return False
+
+
+def test_spawn(world):
+ print(">>> 测试车辆生成...")
+ try:
+ enable_sync_mode(world, 20)
+ v = spawn_ego_vehicle(world)
+ for _ in range(5): world.tick()
+ print(" PASS: %s" % v.type_id)
+ destroy_actors([v]); disable_sync_mode(world)
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e); return False
+
+
+def run_all():
+ print("=" * 50 + "\n CARLA 连接测试\n" + "=" * 50)
+ results = {}
+ client, world = test_connection()
+ results["connection"] = client is not None
+ if world:
+ results["sync"] = test_sync_mode(world)
+ results["spawn"] = test_spawn(world)
+ passed = sum(1 for v in results.values() if v)
+ print("\n结果: %d/%d 通过" % (passed, len(results)))
+ for k, v in results.items(): print(" %s: %s" % (k, "PASS" if v else "FAIL"))
+ return passed == len(results)
+
+
+if __name__ == "__main__":
+ import sys; sys.exit(0 if run_all() else 1)
diff --git a/critical/tests/test_scenario.py b/critical/tests/test_scenario.py
new file mode 100644
index 00000000..b9dc8298
--- /dev/null
+++ b/critical/tests/test_scenario.py
@@ -0,0 +1,53 @@
+# tests/test_scenario.py
+# 测试 10 种场景加载和初始化
+
+import sys, os
+sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+
+from scenarios import SCENARIO_REGISTRY
+
+
+def test_load(name):
+ print(" >>> %s" % name)
+ try:
+ cls = SCENARIO_REGISTRY[name]; s = cls()
+ assert s.name and s.weather and s.ego_speed_ms > 0
+ assert s.category in ("extreme_weather", "vehicle_adversarial", "pedestrian_danger", "multi_factor_coupled")
+ print(" PASS: %s category=%s speed=%.1f km/h" % (s.name, s.category, s.ego_speed_ms * 3.6))
+ return True
+ except Exception as e:
+ print(" FAIL: %s" % e); return False
+
+
+def run_all(quick=False):
+ print("=" * 50 + "\n 场景测试 (%d)\n" % len(SCENARIO_REGISTRY) + "=" * 50)
+ results = {}
+ for name in SCENARIO_REGISTRY:
+ results[name] = test_load(name)
+ if not quick:
+ try:
+ s = SCENARIO_REGISTRY[name]()
+ s.setup()
+ assert s.ego_vehicle is not None
+ s.cleanup()
+ results["%s_lifecycle" % name] = True
+ except Exception as e:
+ print(" FAIL lifecycle: %s" % e)
+ results["%s_lifecycle" % name] = False
+ passed = sum(1 for v in results.values() if v)
+ print("\n结果: %d/%d 通过" % (passed, len(results)))
+ for k, v in results.items():
+ if not v: print(" 失败: %s" % k)
+ return passed == len(results)
+
+
+if __name__ == "__main__":
+ import argparse
+ p = argparse.ArgumentParser()
+ p.add_argument("--scenario", type=str)
+ p.add_argument("--quick", action="store_true")
+ args = p.parse_args()
+ if args.scenario:
+ test_load(args.scenario)
+ else:
+ run_all(quick=args.quick)
diff --git a/critical/undergraduate/content/authorization.tex b/critical/undergraduate/content/authorization.tex
new file mode 100644
index 00000000..ac6bcfda
--- /dev/null
+++ b/critical/undergraduate/content/authorization.tex
@@ -0,0 +1,27 @@
+%!TEX root = ../hutbthesis_main.tex
+
+\clearpage
+\thispagestyle{plain}
+\begin{center}
+{\fontsize{22pt}{28pt}\selectfont\heiti 湖南工商大学本科毕业设计}\par
+\vspace{1.2cm}
+{\fontsize{22pt}{28pt}\selectfont\heiti 版权使用授权书}\par
+\end{center}
+
+\vspace{1.0cm}
+\setlength{\parindent}{2em}
+{\fontsize{14pt}{24pt}\selectfont\songti
+本毕业论文《基于模拟器的极端驾驶仿真场景的生成算法研究》是本人在校期间所完成学业的组成部分,是在学校教师的指导下完成的。因此,本人特授权学校可将本毕业设计的全部或部分内容编入有关书籍、数据库保存,可采用复制、印刷、网页制作等方式将设计文本和经过编辑、批注等处理的设计文本提供给读者查阅、参考,可向有关学术部门和国家有关教育主管部门呈送复印件和电子文档。本毕业设计无论做何种处理,必须尊重本人的著作权,署明本人姓名。
+\par}
+
+\vfill
+\begin{flushright}
+{\fontsize{14pt}{22pt}\selectfont\songti
+\begin{tabular}{rlcl}
+设计作者(签字): & \includegraphics[height=1.3cm,keepaspectratio]{images/auth_author.png} & \hspace{1.5cm} 时间 & 2026年5月25日 \\[1.2cm]
+指导教师已阅(签字): &
+\includegraphics[height=1.4cm,keepaspectratio]{images/auth_teacher.png} &
+\hspace{1.5cm}时间 & 2026年5月25日 \\
+\end{tabular}}
+\end{flushright}
+\clearpage
diff --git a/critical/undergraduate/content/chapter2.tex b/critical/undergraduate/content/chapter2.tex
index 450a0564..d450ba43 100644
--- a/critical/undergraduate/content/chapter2.tex
+++ b/critical/undergraduate/content/chapter2.tex
@@ -38,17 +38,17 @@ \subsection{强化学习基本框架}
强化学习的基本框架由以下五个要素组成:
-(1)状态(State, s ∈ S):是对于某一个时间点而言整个系统的总况,在自动驾驶中,这个概念是由很多个参数组成的,比如目标车所在的位置、速度以及方向,还需要加上周围的物体的位置、运动轨迹还有它们的变化情况等信息。
+(1)状态(State, $s ∈ S$):是对于某一个时间点而言整个系统的总况,在自动驾驶中,这个概念是由很多个参数组成的,比如目标车所在的位置、速度以及方向,还需要加上周围的物体的位置、运动轨迹还有它们的变化情况等信息。
-(2)动作(Action, a ∈ A):智能体能够采取的行为方式,在本文中对抗车辆的动作有加速、减速、转向、变道等控制命令。
+(2)动作(Action, $a ∈ A$):智能体能够采取的行为方式,在本文中对抗车辆的动作有加速、减速、转向、变道等控制命令。
-(3)状态转移概率(Transition Probability, P(s'|s,a)):它是从给定的状态s以及采取的动作a出发,系统会以某个概率进入新的状态s'的概率分布,在确定性环境下,这个过程就是一种一一对应的关系。
+(3)状态转移概率(Transition Probability, $P(s'|s,a)$):它是从给定的状态$s$以及采取的动作$a$出发,系统会以某个概率进入新的状态$s'$的概率分布,在确定性环境下,这个过程就是一种一一对应的关系。
-(4)奖励函数(Reward Function,记作R(s,a)):该函数用来衡量智能体处于某个状态s下采取一个行为a所得到的即时数值化的回报大小。设计良好的奖励函数对于引导智能体的行为以及使智能体达成目标有着至关重要的作用。
+(4)奖励函数(Reward Function,记作$R(s,a)$):该函数用来衡量智能体处于某个状态s下采取一个行为a所得到的即时数值化的回报大小。设计良好的奖励函数对于引导智能体的行为以及使智能体达成目标有着至关重要的作用。
-(5)折现率(Discount factor, γ ∈ [0, 1]):用来衡量现在所获得收益与将来可能得到回报之间关系的重要因素,在数值上越接近1说明决策者更注重长远利益以及它所带来的好处。
+(5)折现率(Discount factor, $γ ∈ [0, 1]$):用来衡量现在所获得收益与将来可能得到回报之间关系的重要因素,在数值上越接近1说明决策者更注重长远利益以及它所带来的好处。
-强化学习目标就是寻求最优策略π*,使从某个初始状态出发所获得的总折现奖励期望最大,即:
+强化学习目标就是寻求最优策略$\pi^*$,使从某个初始状态出发所获得的总折现奖励期望最大,即:
\begin{equation}
\pi^* = \arg\max_{\pi} \mathbb{E}_{\pi}\left[ \sum_{t=0}^{\infty} \gamma^t R(s_t, a_t) \right]
\end{equation}
@@ -56,7 +56,7 @@ \subsection{强化学习基本框架}
\subsection{DQN算法原理}
深度Q网络(Deep Q-Network, DQN)是在2013年由DeepMind提出的,最大的贡献是把深度神经网络应用到经典的强化学习中Q-learning当中,在此之前大多数的方法都是基于一些简单的特征表示进行学习,而DQN则可以处理高维的状态空间,可以从大量的、复杂的、多维的观测(比如图片等)直接得到相应的动作,这极大地扩大了强化学习的应用范围并且推动了该领域的发展\cite{xiu2024dqn}。
-深度强化学习(DQN)的基本思路为用一个较深的神经网络拟合状态动作价值函数(Q(s,a)),这个值表示在给定状态下采取某个动作后能得到的平均未来奖励,而它的最终目的是让经验数据学习尽可能接近贝尔曼最优方程给出的目标。
+深度强化学习(DQN)的基本思路为用一个较深的神经网络拟合状态动作价值函数($Q(s,a)$),这个值表示在给定状态下采取某个动作后能得到的平均未来奖励,而它的最终目的是让经验数据学习尽可能接近贝尔曼最优方程给出的目标。
\begin{equation}
Q^{*}(s,a) = R(s,a) + \gamma \sum_{s'} P(s'|s,a) \max_{a'} Q^{*}(s',a')
\end{equation}
@@ -70,11 +70,11 @@ \subsection{DQN算法原理}
L(\theta) = \mathbb{E}_{(s,a,r,s') \sim D} \left[ \left( r + \gamma \max_{a'} Q(s',a';\theta^-) - Q(s,a;\theta) \right)^2 \right]
\end{equation}
-其中θ是主网络的参数,θ−是目标网络的参数。
+其中$\theta$是主网络的参数,$\theta^-$是目标网络的参数。
由于DQN算法对于离散的动作空间具有良好的适用性,在该应用场景下,可以将目标车辆可进行的操作对应到一定数量离散的状态集(例如加速、减速、变道等),利用DQN来学习各个状态下最优的动作。
\subsection{PPO算法原理}
-一种基于OpenAI于2017年提出的一种新的策略梯度优化方法——近端策略优化(Proximal Policy Optimization, PPO),它打破了传统的强化学习的思想是以价值函数为中心的思想。不同于以往使用价值函数来进行间接选择的方法,PPO是直接对策略网络π(a|s)进行优化得到每一个状态下的各个动作的概率大小。这使得PPO具有更好的效果以及更广泛的应用场景。
+一种基于OpenAI于2017年提出的一种新的策略梯度优化方法——近端策略优化(Proximal Policy Optimization, PPO),它打破了传统的强化学习的思想是以价值函数为中心的思想。不同于以往使用价值函数来进行间接选择的方法,PPO是直接对策略网络$\pi(a|s)$进行优化得到每一个状态下的各个动作的概率大小。这使得PPO具有更好的效果以及更广泛的应用场景。
Proximal Policy Optimization (PPO) 的主要思想是在策略更新时找到一个在每一轮都可使算法得到改进同时又不至于因为调整过大而导致不稳定的方法,在此基础之上提出了梯度裁剪的方法来限制每个训练步骤中对策略参数的改变量,进而避免由于改变过多带来的问题。
@@ -114,7 +114,8 @@ \subsection{DQN与PPO的对比分析}
\centering
\caption{DQN与PPO算法对比表}
\label{tab:dqn_ppo_compare}
- \begin{tabular}{ccc}
+ \small
+ \begin{tabular}{p{2.5cm}p{4.2cm}p{4.2cm}}
\toprule
对比维度 & DQN & PPO \\
\midrule
@@ -136,7 +137,7 @@ \subsection{极端驾驶场景的定义}
(1)功能场景(Functional Scenario):用自然语言对主要因素进行说明并且尽可能少提及具体的数字或者量化的数值等,比如:在下雨天的情况下,前车突然实施紧急制动。
-(2)逻辑场景(Logical Scenario):用参数区间描述场景中各个元素所具有的取值范围。比如降雨强度介于60%-90%,前车加速度为-4到-8m/s²,两车间距为10到30m。
+(2)逻辑场景(Logical Scenario):用参数区间描述场景中各个元素所具有的取值范围。比如降雨强度介于60\%-90\%,前车加速度为-4到-8m/s²,两车间距为10到30m。
(3)具体场景(Concrete Scenario):为逻辑场景中的各个参数设置具体的值从而得到可以在仿真中运行的具体例子。
@@ -160,7 +161,7 @@ \subsection{场景分类体系}
极端天气条件下危险主要是由于外界环境影响感知系统及控制系统而造成的危险。
-(1)大雨情况:下雨对摄像头成像以及激光雷达点云有较大影响,而且湿滑路面使得汽车刹车距离延长30%-50%。
+(1)大雨情况:下雨对摄像头成像以及激光雷达点云有较大影响,而且湿滑路面使得汽车刹车距离延长30\%-50\%。
(2)大雾环境:由于大雾天气下光散射造成画面反差很小,在能见度小于50m情况下很难识别行人。
@@ -172,7 +173,7 @@ \subsection{场景分类体系}
(1) 前车急刹车:前车采用较大加速度(-4m/s²到-8m/s²)进行急刹车,考察主车反应时间以及制动能力。
-(2) 强行加塞:对抗车辆从旁边车道以较近距离进入主车前面,切入角度为10°-20°,切入时间为1s-3s.
+(2) 强行加塞:对抗车辆从旁边车道以较近距离进入主车前面,切入角度为$10°-20°$,切入时间为1s-3s.
(3) 侧向车道逼近:对向车辆不断向主车所在车道靠近,占用主车行车空间,横向偏移速度0.2m/s~0.5m/s。
@@ -237,25 +238,27 @@ \subsection{场景分类体系}
\subsection{典型危险场景的确定}
-根据以上分类,本文从这10类中挑选出10个有代表性的极端驾驶场景用于生成以及测试的基础场景集,如表\ref{tab:scene_list}所示。场景的选择考虑其代表性、可生成性、可量化性以及多样性\cite{zeng2021autonomous}。
-\begin{table}[htbp]
+
+\begin{table}[H]
\centering
\caption{10种典型极端驾驶场景列表}
\label{tab:scene_list}
- \begin{tabular}{ccccc}
+ \small
+ \begin{tabular}{p{1cm}p{2.5cm}p{1.8cm}p{4.5cm}p{3cm}}
\toprule
编号 & 场景名称 & 类别 & 关键参数 & 危险来源 \\
\midrule
1 & 大雨天气跟车 & 极端天气 & 降雨量80\%,能见度100m & 感知退化+制动距离增加 \\
2 & 浓雾天气巡航 & 极端天气 & 能见度50m & 探测距离受限 \\
3 & 夜间城市道路 & 极端天气 & 光照度0.5 lux & 低光照感知挑战 \\
- 4 & 前车紧急制动 & 车辆对抗 & 减速度$-6\mathrm{m/s^2}$,初始距离20m & 突发制动+反应时间 \\
- 5 & 旁车强行加塞 & 车辆对抗 & 切入角度$15^\circ$,切入时间2s & 突然变道+安全距离不足 \\
+ 4 & 前车紧急制动 & 车辆对抗 & 减速度$-6\mathrm{m/s^2}$,初始距离20m & 突发制动 + 反应时间 \\
+ 5 & 旁车强行加塞 & 车辆对抗 & 切入角度$15^\circ$,切入时间2s & 突然变道 + 安全距离不足 \\
6 & 行人横穿马路 & 行人危险 & 横穿速度1.5m/s,初始距离30m & 行人的不确定性 \\
7 & 鬼探头 & 行人危险 & 遮挡物后冲出,纵向距离10m & 感知盲区+极短反应时间 \\
8 & 行人闯红灯 & 行人危险 & 主车绿灯,行人红灯横穿 & 违反预期+突发性 \\
- 9 & 夜间行人横穿 & 多因素耦合 & 光照0.5 lux+横穿速度1.5m/s & 低光照+行人不确定性 \\
- 10 & 雾天鬼探头 & 多因素耦合 & 能见度50m+遮挡物后冲出 & 探测受限+感知盲区 \\
+ 9 & 夜间行人横穿 & 多因素耦合 & 光照0.5 lux + 横穿速度1.5m/s & 低光照 + 行人不确定性 \\
+ 10 & 雾天鬼探头 & 多因素耦合 & 能见度50m + 遮挡物后冲出 & 探测受限 + 感知盲区 \\
\bottomrule
\end{tabular}
-\end{table}
\ No newline at end of file
+\end{table}
+根据以上分类,本文从这10类中挑选出10个有代表性的极端驾驶场景用于生成以及测试的基础场景集,如表\ref{tab:scene_list}所示。场景的选择考虑其代表性、可生成性、可量化性以及多样性\cite{zeng2021autonomous}。
diff --git a/critical/undergraduate/content/chapter3.tex b/critical/undergraduate/content/chapter3.tex
index afdfa4b4..5e31f1c2 100644
--- a/critical/undergraduate/content/chapter3.tex
+++ b/critical/undergraduate/content/chapter3.tex
@@ -46,7 +46,7 @@ \subsection{状态空间设计}
(5)碰撞标志 $C_{\text{collision}}$
-作为一种二元逻辑变量,碰撞标志用以表示上一时间片是否出现过碰撞情况。它可以给智能体提供直接负反馈信息,使其改善自身躲避障碍物方法以及改善运动规划结果。该变量只有两种取值,即0或者1:0表示无碰撞;1表示有碰撞。
+作为一种二元逻辑变量,碰撞标志用以表示上一时间片是否出现过碰撞情况。它可以给智能体提供直接负反馈信息,使其改善自身躲避障碍物方法以及改善运动规划结果。该变量只有两种取值,即0或者1;0表示无碰撞;1表示有碰撞。
\begin{equation}
C_{\text{collision}} \in \{0, 1\}
\end{equation}
@@ -105,9 +105,9 @@ \subsection{奖励函数设计}
(一)危险度奖励 $R_{\text{danger}}$
-作为一个重要组成部分,危险性评估模块对主车的安全起到决定性作用,在本课题中采用碰撞时间(Time to Collision, TTC)来衡量危险的程度,即在当前相对运动状态下,两辆车或者多辆车之间发生碰撞所需要的时间,数值越小说明在短时间内受到撞击的可能性越大,从而使得整个系统的安全性大幅下降。
+作为一个重要组成部分,危险性评估模块对主车的安全起到决定性作用,在本课题中采用碰撞时间($Time to Collision, TTC$)来衡量危险的程度,即在当前相对运动状态下,两辆车或者多辆车之间发生碰撞所需要的时间,数值越小说明在短时间内受到撞击的可能性越大,从而使得整个系统的安全性大幅下降。
-TTC计算方法在第二章已经介绍,在此不再赘述。而根据TTC大小进行危险度奖励也是用一段函数表示:
+$TTC$计算方法在第二章已经介绍,在此不再赘述。而根据$TTC$大小进行危险度奖励也是用一段函数表示:
\begin{equation}
R_{\text{danger}} =
\begin{cases}
@@ -147,7 +147,7 @@ \subsection{奖励函数设计}
R_{\text{speed}} = +0.11 \times \min\left( \frac{v_{\text{adv}}}{20.0}, 1.0 \right)
\end{equation}
-其中,速度v是用每小时千米来表示汽车行驶速度。而这个激励有以下特点:随车速增加而增加奖励,在一定范围内呈饱和状态并且最大值为0.1;当车速小于等于20公里每小时时候,奖励会按比例减少,使智能体能够控制自身速度。但是相比于可能出现惩罚力度来说,这个奖励太高,所以很明显它主要目的是避免发生交通事故。
+其中,速度$v$是用每小时千米来表示汽车行驶速度。而这个激励有以下特点:随车速增加而增加奖励,在一定范围内呈饱和状态并且最大值为0.1;当车速小于等于20公里每小时时候,奖励会按比例减少,使智能体能够控制自身速度。但是相比于可能出现惩罚力度来说,这个奖励太高,所以很明显它主要目的是避免发生交通事故。
(五)总奖励函数
@@ -165,7 +165,7 @@ \subsection{引入注意力机制对DQN的优化}
\text{Attention}(Q, K, V) = \text{softmax}\left( \frac{QK^T}{\sqrt{d_k}} \right) V
\end{equation}
-其中,$d_k$ 表示键向量维度,主要用于缩放处理,避免点积数值过大导致梯度饱和。该缩放点积注意力机制也是Transformer模型的重要组成部分,并且已经被证明具备优异的特征提取能力。
+其中,$d_k$表示键向量维度,主要用于缩放处理,避免点积数值过大导致梯度饱和。该缩放点积注意力机制也是Transformer模型的重要组成部分,并且已经被证明具备优异的特征提取能力。
在深度强化学习任务中,智能体需要从高维状态空间当中提取关键特征来完成决策。传统DQN采用全连接网络对状态向量进行处理,各状态维度被同等对待,缺少对关键特征的筛选能力。引入注意力机制后,可以有效弥补这一不足,使智能体能够动态关注与当前决策关系最密切的状态信息,从而提高学习效率以及决策质量。
@@ -177,7 +177,7 @@ \subsection{引入注意力机制对DQN的优化}
(二)提升样本效率和收敛速度
-通过更加有效地提取状态特征,注意力机制能够减少训练过程所需样本数量。实验预期表明,相较于标准 DQN,Attention-DQN 可以在更少训练回合内完成收敛,样本利用效率预计提高约20%~30%。这一优势在计算资源受限条件下具有较高的实际意义。
+通过更加有效地提取状态特征,注意力机制能够减少训练过程所需样本数量。实验预期表明,相较于标准 DQN,Attention-DQN 可以在更少训练回合内完成收敛,样本利用效率预计提高约20\%~30\%。这一优势在计算资源受限条件下具有较高的实际意义。
(三)增强对噪声的鲁棒性
@@ -192,7 +192,7 @@ \subsection{DQN算法的局限性分析}
(二)确定性策略的探索能力有限
-传统的ε-贪心算法在深度强化学习中的应用主要是选择当前状态下具有最大Q值得动作再附加一定比例的小概率随机探索实现开发与探索之间的权衡。虽然由于其实现较为简洁而被广泛使用,但是也存在一些缺点,在一些复杂或者变化较大的环境中,其缺点就体现出来了,比如在极端恶劣天气下,只有靠ε-贪心是不能很好地覆盖所有情况,从而导致学习效果不佳以及学到的方法不够鲁棒。
+传统的$\varepsilon$-贪心算法在深度强化学习中的应用主要是选择当前状态下具有最大Q值得动作再附加一定比例的小概率随机探索实现开发与探索之间的权衡。虽然由于其实现较为简洁而被广泛使用,但是也存在一些缺点,在一些复杂或者变化较大的环境中,其缺点就体现出来了,比如在极端恶劣天气下,只有靠$\varepsilon$-贪心是不能很好地覆盖所有情况,从而导致学习效果不佳以及学到的方法不够鲁棒。
(三)训练稳定性问题
@@ -226,7 +226,7 @@ \subsection{PPO算法的网络结构与奖励重塑}
Actor网络接收一个五维的状态向量并产生在连续动作空间中的概率分布的重要参数作为输出。不同于传统的基于固定大小的动作集做出选择的方法,在这里本文使用Actor网络来处理连续动作问题,希望它能产生可以代表动作分布的一些东西。这些输出包括两个部分:
-动作均值μ:一个三维向量,表示油门、刹车、转向这三个动作上的中心点;动作标准差σ:也是一个三维向量,用来决定动作采样的范围大小。
+动作均值$\mu$:一个三维向量,表示油门、刹车、转向这三个动作上的中心点;动作标准差$\sigma$:也是一个三维向量,用来决定动作采样的范围大小。
在训练过程中,策略网络产生的动作的概率分布被建模成对角高斯分布,即认为每一个动作都是一个相互独立的随机变量并且各自符合一个独立的高斯分布,在此基础上采样得到具体的动作。
\begin{equation}
@@ -247,7 +247,7 @@ \subsection{PPO算法的网络结构与奖励重塑}
L^{VF}(\phi) = \frac{1}{T} \sum_{t=1}^{T} \left( V_\phi(s_t) - \hat{R}_t \right)^2
\end{equation}
-其中是从时间步t开始的真实累计奖励,是 critic 网络所估计的值。
+其中$\hat{R}_t$是从时间步t开始的真实累计奖励,$V_\phi(s_t)$是 critic 网络所估计的值。
(3)网络层级参数设置
@@ -255,15 +255,15 @@ \subsection{PPO算法的网络结构与奖励重塑}
输入层:5个神经元,接收归一化后的状态向量;
-第一隐藏层:128个神经元,使用ReLU激活函数,输出维度128。
+第一隐藏层:128个神经元,使用$ReLU$激活函数,输出维度128。
-第二个隐藏层:128个神经元,使用ReLU作为激活函数,输出维度为128。
+第二个隐藏层:128个神经元,使用$ReLU$作为激活函数,输出维度为128。
-Actor输出层:6个神经元(3个均值+3个标准差),使用tanh激活函数使均值在一定范围内。
+Actor输出层:6个神经元(3个均值+3个标准差),使用$tanh$激活函数使均值在一定范围内。
Critic输出层:一个神经元,无激活函数,直接输出标量值。
-在设计神经网络结构上,要兼顾模型效果以及计算成本。实验结果显示,设置128个隐藏层神经元可以很好地拟合状态-动作之间的关系,同时也能较好地避免过拟合现象并且加快训练速度。本文使用ReLU作为激活函数,因为其具有速度快、传梯度好等优点,能很好地解决传统的激活函数容易出现的梯度消失的问题。
+在设计神经网络结构上,要兼顾模型效果以及计算成本。实验结果显示,设置128个隐藏层神经元可以很好地拟合状态-动作之间的关系,同时也能较好地避免过拟合现象并且加快训练速度。本文使用$ReLU$作为激活函数,因为其具有速度快、传梯度好等优点,能很好地解决传统的激活函数容易出现的梯度消失的问题。
(二)连续动作空间建模
@@ -276,7 +276,7 @@ \subsection{PPO算法的网络结构与奖励重塑}
油门开度是一个位于[0,1]之间实数值,用来表示汽车加速度大小,在油门开度为0时,说明司机已经把油门完全松开,这时汽车主要是靠自身惯性来降低速度;而在油门开度为1时,则是把油门踩到底,使汽车具有最大加速度。不同于DQN只能给出离散选择,采用PPO(Proximal Policy Optimization)算法强化学习可以得到连续实数值,这有利于更精准控制发动机扭矩以及提高整个自动驾驶过程舒适性和效率。
-制动力度在[0,1]范围内取值,该值表示汽车减速程度。即数值为0表示制动不起作用,数值为1时表示已经施加最大制动力,一般情况下这将导致ABS起作用。为了模拟紧急制动情况,在程序中使用一个可变制动力度进行制动操作,从开始时较小制动力度(如0.3)逐渐增加到较大制动力度(如0.8),相比于一般强化学习所使用的固定减速率方法,这种方法更加符合实际情况同时也对目标检测车辆避险提出更高要求。
+制动力度在[0,1]范围内取值,该值表示汽车减速程度。即数值为0表示制动不起作用,数值为1时表示已经施加最大制动力,一般情况下这将导致$ABS$起作用。为了模拟紧急制动情况,在程序中使用一个可变制动力度进行制动操作,从开始时较小制动力度(如0.3)逐渐增加到较大制动力度(如0.8),相比于一般强化学习所使用的固定减速率方法,这种方法更加符合实际情况同时也对目标检测车辆避险提出更高要求。
当转向角度位于[-0.5, 0.5]区间时,该参数主要作用是控制汽车左右移动,不同数值改变会对变道以及转向产生较大影响,负数为向左打方向,正值为向右打方向,数值越大说明方向盘转动程度越大,即绝对值等于0.5时为最大转向角度(约25°),0.1为较小的方向盘转动量。
@@ -297,7 +297,7 @@ \subsection{PPO算法的网络结构与奖励重塑}
R_{\text{smooth}} = -0.1 \times \| a_t - a_{t-1} \|_2
\end{equation}
-上述公式中用、分别表示当前时刻以及上一时刻的动作向量,而使用的是欧几里得范数计算两者之差值大小。这个奖励是为了让模型注意到相邻两个时间点之间行为的变化情况,在两者差距很大时给予较大的负奖励。由于欧几里得范数考虑多个维度的动作参数共同作用,因此这样做可以更全面地反映一个行为序列的整体趋势以及它们之间的关系而不只是某个方面。
+上述公式中用$a_t$、$a_{t-1}$分别表示当前时刻以及上一时刻的动作向量,而使用$\|\cdot\|_2$的是欧几里得范数计算两者之差值大小。这个奖励是为了让模型注意到相邻两个时间点之间行为的变化情况,在两者差距很大时给予较大的负奖励。由于欧几里得范数考虑多个维度的动作参数共同作用,因此这样做可以更全面地反映一个行为序列的整体趋势以及它们之间的关系而不只是某个方面。
平滑奖励旨在让智能体产生连贯并且有动态的行为序列,使它更自然地与外界进行交互,在交通场景切换的任务上,这个奖励可以使智能体以逐渐增加速度以及缓慢改变方向的方式进行操作,避免了之前那种突然加速、减速还有大幅度转方向的情况发生,这大大提升了模拟的效果并且也为之后将生成的数据转换成符合OpenSCENARIO标准格式做了一个很好的铺垫工作因为连续轨迹比离散轨迹更容易理解也更精确。
@@ -322,7 +322,7 @@ \subsection{算法生成流程}
步骤三:状态获取与动作选择
-在每次仿真过程中,需从CARLA仿真环境中获取目标车辆以及其可能碰撞的对象的主要信息并用预先定义的形式表示成五维状态,为了使该算法具有较好的鲁棒性和稳定性,对这五个状态变量进行标准化处理之后再送入到DQN中,然后用ε-贪心法产生具体的动作,在开始的时候可以将ε设为1.0以便让网络更好的学习,当经过一段时间的学习以后再将ε降低到0.01,这样可以让网络慢慢学会选择更好的行为。
+在每次仿真过程中,需从CARLA仿真环境中获取目标车辆以及其可能碰撞的对象的主要信息并用预先定义的形式表示成五维状态,为了使该算法具有较好的鲁棒性和稳定性,对这五个状态变量进行标准化处理之后再送入到DQN中,然后用$\varepsilon$-贪心法产生具体的动作,在开始的时候可以将$\varepsilon$设为1.0以便让网络更好的学习,当经过一段时间的学习以后再将$\varepsilon$降低到0.01,这样可以让网络慢慢学会选择更好的行为。
步骤四:动作执行与环境更新
@@ -349,6 +349,8 @@ \subsection{算法生成结果}
\label{fig:rainy_following_results}
\end{figure}
+
+
Smooth-PPO(橙色曲线)
在整个训练过程中,Smooth-PPO 始终保持较为稳定的上升趋势,奖励值由初始阶段约93.5逐步提高至102以上,仅在第5轮附近出现轻微波动,整体未出现明显震荡。
@@ -433,7 +435,7 @@ \subsection{从 Attention-DQN 到 Smooth-PPO稳定性的演进}
(一)Attention-DQN 的稳定性方案:注意力机制 + 经验回放 + 目标网络
-Attention-DQN 在标准 DQN 基础上引入注意力模块,增强对相对距离、TTC、车道偏移等关键危险特征的聚焦能力,并保留经验回放与目标网络保证训练稳定:注意力机制动态加权状态维度,提升关键危险信息的感知效率;经验回放打破样本时序相关,提升离散动作学习的稳定性:目标网络延迟更新,避免Q值估计震荡与过估计问题。 Attention-DQN 的稳定性提升依赖特征优化与数据层面优化,属于间接稳定手段,在连续控制与湿滑路面、低能见度等复杂场景下仍存在动作跳变、策略波动等不足。
+Attention-DQN 在标准 DQN 基础上引入注意力模块,增强对相对距离、$TTC$、车道偏移等关键危险特征的聚焦能力,并保留经验回放与目标网络保证训练稳定:注意力机制动态加权状态维度,提升关键危险信息的感知效率;经验回放打破样本时序相关,提升离散动作学习的稳定性:目标网络延迟更新,避免$Q$值估计震荡与过估计问题。 Attention-DQN 的稳定性提升依赖特征优化与数据层面优化,属于间接稳定手段,在连续控制与湿滑路面、低能见度等复杂场景下仍存在动作跳变、策略波动等不足。
(二)Smooth-PPO 的稳定性方案:剪切策略更新 + 动作平滑约束
@@ -462,11 +464,11 @@ \subsection{目标函数的数学关联}
(三)数学层面的内在联系
-价值与策略互补:Smooth-PPO 中的优势函数At本质依赖价值估计,与 Attention-DQN 的 Q 值具有数学一致性;
+价值与策略互补:Smooth-PPO 中的优势函数$A_t$本质依赖价值估计,与 Attention-DQN 的 Q 值具有数学一致性;
稳定性目标一致:Attention-DQN 的目标网络与 Smooth-PPO 的剪切机制,都是通过限制更新幅度实现稳定训练;
-危险导向统一:两者奖励函数均以 TTC、碰撞风险、安全距离为核心,目标都是生成高真实性极端驾驶场景。
+危险导向统一:两者奖励函数均以 $TTC$、碰撞风险、安全距离为核心,目标都是生成高真实性极端驾驶场景。
\subsection{联系与区别的总结}
两种算法之间的联系:均面向极端驾驶场景设计,以生成危险对抗行为为目标;均采用多维度状态空间(相对距离、相对速度、车道偏移等);均以安全性、收敛性、场景真实性为优化方向;最终均可输出符合 OpenSCENARIO 标准的.xosc 场景文件。
diff --git a/critical/undergraduate/content/chapter4.tex b/critical/undergraduate/content/chapter4.tex
index 337f322c..826eaecc 100644
--- a/critical/undergraduate/content/chapter4.tex
+++ b/critical/undergraduate/content/chapter4.tex
@@ -7,7 +7,7 @@ \subsection{极端天气类场景生成效果}
(二)浓雾天气巡航场景
-如图\ref{fig:foggy_cruise}在浓雾天气下能见度设置为50米,主车以50km/h速度巡航。Smooth-PPO算法在该场景中展现出良好的鲁棒性,能够在能见度受限的条件下有效逼近主车。生成场景的最小TTC为2.0秒,危险成功率为79\%。值得注意的是,Smooth-PPO的随机策略使其在浓雾环境中具有更强的适应性,收敛速度较Attention-DQN提升约36\%。
+如图\ref{fig:foggy_cruise}在浓雾天气下能见度设置为50米,主车以50km/h速度巡航。Smooth-PPO算法在该场景中展现出良好的鲁棒性,能够在能见度受限的条件下有效逼近主车。生成场景的最小$TTC$为2.0秒,危险成功率为79\%。值得注意的是,Smooth-PPO的随机策略使其在浓雾环境中具有更强的适应性,收敛速度较Attention-DQN提升约36\%。
\begin{figure}[htbp]
\centering
\begin{minipage}{0.45\textwidth}
@@ -63,7 +63,7 @@ \subsection{对抗车辆类场景生成效果}
(一)前车紧急制动场景
-前车紧急制动是测试主车反应能力的经典场景。如图\ref{fig:emergency_braking}对抗车辆需要在主车接近时触发紧急制动,以最小化主车的安全距离。Smooth-PPO算法学习到了渐进式制动策略,制动过程分为预警制动、持续压缩和极限增强三个阶段,使TTC呈现平滑下降形态。与Attention-DQN的触发式急刹相比,Smooth-PPO生成的制动曲线更加平滑,对抗行为更加真实自然,接近人类驾驶中的点刹操作,能够在保持高危险度的同时有效控制碰撞风险。
+前车紧急制动是测试主车反应能力的经典场景。如图\ref{fig:emergency_braking}对抗车辆需要在主车接近时触发紧急制动,以最小化主车的安全距离。Smooth-PPO算法学习到了渐进式制动策略,制动过程分为预警制动、持续压缩和极限增强三个阶段,使$TTC$呈现平滑下降形态。与Attention-DQN的触发式急刹相比,Smooth-PPO生成的制动曲线更加平滑,对抗行为更加真实自然,接近人类驾驶中的点刹操作,能够在保持高危险度的同时有效控制碰撞风险。
(二)旁车强行加塞场景
@@ -135,7 +135,7 @@ \subsection{多因素耦合场景生成效果}
\section{生成场景的.xosc文件输出}
\subsection{.xosc文件的生成方法与流程}
-OpenSCENARIO是ASAM(自动化及测量系统标准协会)制定的自动驾驶仿真场景描述标准,采用XML格式定义动态交通场景。本研究采用Python编程语言结合开源库scenariogeneration进行.xosc文件的自动生成。该库提供了完整的OpenSCENARIO XML构建接口,支持场景参数化定义和批量生成\cite{yu2021openscenario}。整体生成流程如图~\ref{fig:xosc_generation_flow}所示。
+OpenSCENARIO是ASAM(自动化及测量系统标准协会)制定的自动驾驶仿真场景描述标准,采用XML格式定义动态交通场景。本研究采用Python编程语言结合开源库$scenariogeneration$进行.xosc文件的自动生成。该库提供了完整的OpenSCENARIO XML构建接口,支持场景参数化定义和批量生成\cite{yu2021openscenario}。整体生成流程如图~\ref{fig:xosc_generation_flow}所示。
\begin{figure}[htbp]
\centering
\includegraphics[width=0.8\textwidth]{figure_19.png}
@@ -162,62 +162,192 @@ \subsection{.xosc文件的生成方法与流程}
(5)场景验证与 CARLA 仿真复现:
最后,将生成的.xosc文件导入 CARLA 模拟器中进行加载与运行,验证场景的可复现性与逻辑正确性,确保生成的场景能够在仿真环境中按预期执行。
-
\subsection{.xosc文件实现}
-以 ASAM OpenSCENARIO 1.2 为标准,预定义 XML 模板 template.xosc,采用 \texttt{\{\{key\}\}} 占位符标记动态字段,\texttt{\{\{\#section\}\}}...\texttt{\{\{/section\}\}} 标记条件块。模板包含 FileHeader、ParameterDeclarations、RoadNetwork、Entities、Storyboard 等核心标签。
-
+以 $ASAM OpenSCENARIO$ 1.2 为标准,预定义 XML 模板 $template.xosc$,采用 \texttt{\{\{key\}\}} 占位符标记动态字段,\texttt{\{\{\#section\}\}}...\texttt{\{\{/section\}\}} 标记条件块。模板包含 FileHeader、ParameterDeclarations、RoadNetwork、Entities、Storyboard 等核心标签。
本文使用旁车加塞的.xosc文件为例,对文件的实现进行说明。
(一)文件头
-
-如图~\ref{fig:xosc_header}所示,声明所遵循的OpenSCENARIO标准版本号,以及文件的创建工具和描述信息。这是XML文件的起始部分,确保后续内容符合标准规范。
-\begin{figure}[htbp]
+\begin{figure}[H]
\centering
- \includegraphics[width=0.8\textwidth]{figure_20.png}
+ \begin{lstlisting}[language=XML]
+
+
+
+
+
+
+ \end{lstlisting}
\caption{.xosc文件头示例}
\label{fig:xosc_header}
\end{figure}
+如图~\ref{fig:xosc_header}所示,声明所遵循的OpenSCENARIO标准版本号,以及文件的创建工具和描述信息。这是XML文件的起始部分,确保后续内容符合标准规范。
+
+
+
+
+
+
+
(二)参数声明
如图~\ref{fig:xosc_parameters}所示,定义场景中可调节的关键参数变量,如主车速度、对抗车辆速度、初始相对距离、触发阈值等。通过参数声明机制,用户可以批量修改参数值,快速生成同一逻辑场景下的多个具体实例,便于参数化测试。
-\begin{figure}[htbp]
+\begin{figure}[H]
\centering
- \includegraphics[width=0.8\textwidth]{figure_21.png}
+ \begin{lstlisting}[language=XML]
+
+
+
+
+
+ \end{lstlisting}
\caption{.xosc文件参数声明示例}
\label{fig:xosc_parameters}
\end{figure}
+
+
+
+
+
+
+
(三)道路网络
如图~\ref{fig:xosc_road_network}所示,指定场景所依托的地图文件(OpenDRIVE格式的.xodr文件),定义道路的几何拓扑和车道结构。该部分确保场景中的车辆运动符合道路物理约束。
-\begin{figure}[htbp]
+\begin{figure}[H]
\centering
- \includegraphics[width=0.8\textwidth]{figure_22.png}
+ \begin{lstlisting}[language=XML]
+
+
+
+
+ \end{lstlisting}
\caption{.xosc文件道路网络示例}
\label{fig:xosc_road_network}
\end{figure}
+
+
+
+
+
+
+
+
(四)实体定义
如图~\ref{fig:xosc_entities}所示,声明场景中所有参与的交通参与者,包括主车、对抗车辆、行人等。每个实体需指定其车辆模型(如audi.a2、tesla.model3)、类型以及初始绑定关系。
-\begin{figure}[htbp]
+\begin{figure}[H]
\centering
- \includegraphics[width=0.8\textwidth]{figure_23.png}
+ \begin{lstlisting}[language=XML]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \end{lstlisting}
\caption{.xosc文件实体定义示例}
\label{fig:xosc_entities}
\end{figure}
+
+
+
+
+
+
+
+
+
+
(五)故事板
-\begin{figure}[htbp]
+如图~\ref{fig:xosc_storyboard}所示,场景描述的核心部分,定义场景的动态行为逻辑。故事板由多个行为单元(Act)组成,每个行为单元包含若干操作(Maneuver)和事件(Event),通过触发条件(Condition)来控制实体的动作执行时序。
+
+\begin{figure}[H]
\centering
- \includegraphics[width=0.8\textwidth]{figure_24.png}
+ \begin{lstlisting}[language=XML]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ \end{lstlisting}
\caption{.xosc文件故事板示例}
\label{fig:xosc_storyboard}
\end{figure}
-如图~\ref{fig:xosc_storyboard}所示,场景描述的核心部分,定义场景的动态行为逻辑。故事板由多个行为单元(Act)组成,每个行为单元包含若干操作(Maneuver)和事件(Event),通过触发条件(Condition)来控制实体的动作执行时序。
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/critical/undergraduate/content/declarationzh.tex b/critical/undergraduate/content/declarationzh.tex
index 995bef4f..a807a775 100644
--- a/critical/undergraduate/content/declarationzh.tex
+++ b/critical/undergraduate/content/declarationzh.tex
@@ -1,28 +1,12 @@
%!TEX root = ../hutbthesis_main.tex
\begin{declarationzh}
-
-本人郑重声明:所呈交的本科毕业设计(论文)是本人在指导老师的指导下,独立进行研究工作所取得的成果,成果不存在知识产权争议,除文中已经注明引用的内容外,本论文不含任何其他个人或集体已经发表或撰写过的作品成果。
-对本文的研究做出重要贡献的个人和集体均已在文中以明确方式标明。
-本人完全意识到本声明的法律结果由本人承担。
-\\
-\\
-\\
-\\
-\\
-\\
-\\
-\\
-
- \vspace{30pt}
- \begin{tabular}{ll}
- %\renewcommand{\arraystretch}{2}
- \makebox[4em][s]{作者签名:李明浩} & \makebox[100pt][c]{ } \\
- \\
- \makebox[2em][s]{日期:} &
- \makebox[100pt][c]{\qquad2026年\quad5月\quad18日} \\
- \end{tabular}
+本人郑重声明:所呈交的本科毕业设计是本人在指导老师的指导下,独立进行研究工作所取得的成果,成果不存在知识产权争议,除文中已经注明引用的内容外,本设计不含任何其他个人或集体已经发表或撰写过的作品成果。对本文的研究做出重要贡献的个人和集体均已在文中以明确方式标明。本人完全意识到本声明的法律结果由本人承担。
+
+\vspace{5.8cm}
+\noindent 作者签名:\hspace{0.8cm}\includegraphics[height=1.4cm,keepaspectratio]{images/auth_author.png}
+
+\vspace{1.6cm}
+\noindent 日期:\hspace{0.8cm}2026\hspace{1em}年\hspace{1em}5\hspace{1em}月\hspace{1em}25\hspace{1em}日
-
-
\end{declarationzh}
diff --git a/critical/undergraduate/hutbthesis.cls b/critical/undergraduate/hutbthesis.cls
index 83a4b17e..cb98aa1e 100644
--- a/critical/undergraduate/hutbthesis.cls
+++ b/critical/undergraduate/hutbthesis.cls
@@ -619,13 +619,15 @@
\patchcmd{\chapter}{\thispagestyle}{\@gobble}{}{}
% 内芯页眉设置
\ifhutb@type@print
-\fancyhead[L]{\includegraphics[scale=0.10]{hutb_logo_maoti.png}}
+%\fancyhead[L]{\includegraphics[scale=0.10]{hutb_logo_maoti.png}}
\else
-\fancyhead[L]{\includegraphics[scale=0.10]{hutb_logo_maoti.png}}
+%\fancyhead[L]{\includegraphics[scale=0.10]{hutb_logo_maoti.png}}
\fi
% \fancyhf[RH]{\heiti \zihao{-5} {图像与激光融合的轨道扣件脱落检测}} % 设置所有(奇数和偶数)右侧页眉
% UPDATE 更新配置为论文标题
-\fancyhf[RH]{\heiti \zihao{-5} {\@titlecn}}
+% 清空页眉,具体设置规则参考:https://www.overleaf.com/learn/latex/Headers_and_footers
+\fancyhf{}
+\fancyhf[CH]{\heiti \zihao{-5} 湖南工商大学毕业论文}
% frontmatter设置
\renewcommand{\frontmatter}{
\cleardoublepage
@@ -792,7 +794,7 @@
\vspace{40pt}
% \vfill
\begingroup
- {\zihao{2} \heiti 人工智能与先进计算学院 \par}
+ {\zihao{2} \heiti \@department \par}
\vspace{10pt}
{\zihao{-2} \heiti \@thesisdate \par}
\endgroup
@@ -1108,7 +1110,7 @@ chapter = {%
% afterskip = {\ifhutb@type@graduate 30pt\else 20pt\fi},
},
section = {%
- format = \zihao{-4} \heiti,
+ format = \zihao{-4} \bfseries \songti,
afterindent = true,
% beforeskip 默认为 3.5ex plus 1ex minus .2ex 适当缩减
% beforeskip = {20pt},
@@ -1117,7 +1119,7 @@ section = {%
afterskip = {1ex \@plus .2ex},
},
subsection = {%
- format = \zihao{-4} \bfseries \songti,
+ format = \zihao{-4} \songti,
afterindent = true,
% afterskip 默认为 2.3ex plus .2ex 适当缩减
afterskip = {1ex \@plus .2ex},
diff --git a/critical/undergraduate/hutbthesis_main.tex b/critical/undergraduate/hutbthesis_main.tex
index 0916a90b..a4f2408d 100644
--- a/critical/undergraduate/hutbthesis_main.tex
+++ b/critical/undergraduate/hutbthesis_main.tex
@@ -38,7 +38,7 @@
%\include{content/info}
\include{content/declarationzh}
-
+\include{content/authorization}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% 中文摘要
%
@@ -102,7 +102,7 @@
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% \section{参考文献} % bibliography会自动显示参考文献四个字
\addcontentsline{toc}{chapter}{参考文献} % 由于参考文献不是chapter,这句把参考文献加入目录
-% \nocite{*} % 该命令用于显示全部参考文献,即使文中没引用
+\nocite{*} % 该命令用于显示全部参考文献,即使文中没引用
% cls文件中已经引入package,这里不需要调用 \bibliographystyle 了。
%\bibliographystyle{gbt7714-2005}
%\bibliography{reference}
@@ -129,8 +129,7 @@
% 在修改过程中请注意不要破环命令的完整性
% \renewcommand\appendix{\setcounter{secnumdepth}{-2}}
-\appendix
-\include{content/appendix}
-
+%\appendix
+%\include{content/appendix}
\end{document}
diff --git a/critical/undergraduate/images/auth_author.png b/critical/undergraduate/images/auth_author.png
new file mode 100644
index 00000000..a1c18789
Binary files /dev/null and b/critical/undergraduate/images/auth_author.png differ
diff --git a/critical/undergraduate/images/auth_teacher.png b/critical/undergraduate/images/auth_teacher.png
new file mode 100644
index 00000000..f4689f1f
Binary files /dev/null and b/critical/undergraduate/images/auth_teacher.png differ
diff --git a/critical/utils/__init__.py b/critical/utils/__init__.py
new file mode 100644
index 00000000..934ea17d
--- /dev/null
+++ b/critical/utils/__init__.py
@@ -0,0 +1,39 @@
+# utils/__init__.py
+# 工具模块统一入口
+
+from .carla_utils import (
+ connect_to_carla, enable_sync_mode, disable_sync_mode,
+ spawn_ego_vehicle, spawn_adv_vehicle,
+ spawn_npc_vehicles, spawn_pedestrians, spawn_pedestrian_at, walk_to_location,
+ set_vehicle_speed, apply_brake,
+ get_spawn_points, get_random_spawn_point,
+ destroy_actors, cleanup_all,
+)
+
+from .sensor_utils import (
+ CollisionSensor, RGBCamera, LidarSensor,
+ DistanceMonitor, LaneInvasionSensor,
+)
+
+from .data_saver import (
+ ensure_dir, init_result_dir,
+ save_vehicle_log, save_training_log, load_training_log,
+ save_metrics, load_metrics, save_model, load_model,
+ save_xosc, save_scenario_config, save_experiment_results,
+)
+
+from .metrics import (
+ compute_ttc, compute_thw, compute_collision_probability,
+ danger_level, classify_danger,
+ EpisodeStats, aggregate_episodes, MovingAverage,
+ composite_danger_score, SCENARIO_CATEGORIES, CATEGORY_LABELS_CN,
+)
+
+from .geometry_utils import (
+ distance_2d, distance_3d, distance_between_vehicles,
+ speed_ms, speed_kmh, relative_speed,
+ heading_vector, heading_angle, angle_between, is_facing,
+ get_lane_offset, get_current_lane_id, is_same_lane,
+ trajectory_deviation, get_vehicle_bbox, bbox_overlap,
+ to_local_coordinates, to_world_coordinates,
+)
diff --git a/critical/carla_utils.py b/critical/utils/carla_utils.py
similarity index 100%
rename from critical/carla_utils.py
rename to critical/utils/carla_utils.py
diff --git a/critical/data_saver.py b/critical/utils/data_saver.py
similarity index 100%
rename from critical/data_saver.py
rename to critical/utils/data_saver.py
diff --git a/critical/geometry_utils.py b/critical/utils/geometry_utils.py
similarity index 100%
rename from critical/geometry_utils.py
rename to critical/utils/geometry_utils.py
diff --git a/critical/metrics.py b/critical/utils/metrics.py
similarity index 77%
rename from critical/metrics.py
rename to critical/utils/metrics.py
index 13e38d76..5089b1a0 100644
--- a/critical/metrics.py
+++ b/critical/utils/metrics.py
@@ -215,3 +215,56 @@ def std(self):
if len(self.window) < 2:
return 0.0
return np.std(self.window)
+
+
+# ================================================================
+# 综合危险分数(用于算法跨类别对比柱状图)
+# ================================================================
+
+def composite_danger_score(eval_summary):
+ """
+ 从评估汇总数据计算综合危险分数(0-100)。
+
+ 公式: 0.40 × TTC评分 + 0.30 × 碰撞评分 + 0.20 × 熵评分 + 0.10 × 效率评分
+
+ eval_summary: 来自 EpisodeStats.summary() 或 aggregate_episodes()
+ """
+ # TTC 评分:TTC 越大越安全(TTC > 10s = 满分, TTC < 1s = 0 分)
+ mean_ttc = eval_summary.get("mean_min_ttc", 0)
+ ttc_score = min(100, max(0, (mean_ttc - 1.0) / 9.0 * 100))
+
+ # 碰撞评分:无碰撞 = 满分
+ collision_rate = eval_summary.get("collision_rate", 0)
+ collision_score = (1.0 - collision_rate) * 100
+
+ # 熵评分:行为稳定性(危险等级越低越稳定)
+ mean_danger = eval_summary.get("mean_max_danger_level", 3)
+ entropy_score = (1.0 - mean_danger / 3.0) * 100
+
+ # 效率评分:安全完成率
+ success_rate = eval_summary.get("success_rate", 0)
+ efficiency_score = success_rate * 100
+
+ return 0.40 * ttc_score + 0.30 * collision_score + 0.20 * entropy_score + 0.10 * efficiency_score
+
+
+# 场景类别映射
+SCENARIO_CATEGORIES = {
+ "rain_storm": "extreme_weather",
+ "heavy_fog": "extreme_weather",
+ "tunnel_night": "extreme_weather",
+ "emergency_brake": "vehicle_adversarial",
+ "cut_in": "vehicle_adversarial",
+ "pedestrian_cross": "pedestrian_danger",
+ "ghost_peek": "pedestrian_danger",
+ "jaywalking": "pedestrian_danger",
+ "night_pedestrian": "multi_factor_coupled",
+ "fog_ghost": "multi_factor_coupled",
+}
+
+CATEGORY_LABELS_CN = {
+ "multi_factor_coupled": "多因素耦合类",
+ "pedestrian_danger": "行人危险类",
+ "vehicle_adversarial": "车辆对抗类",
+ "extreme_weather": "极端天气类",
+}
diff --git a/critical/sensor_utils.py b/critical/utils/sensor_utils.py
similarity index 100%
rename from critical/sensor_utils.py
rename to critical/utils/sensor_utils.py