【Python】SUMO + Python 联合仿真平台

第五部分:集成强化学习算法进行交通优化

强化学习的核心思想是让智能体 (Agent) 通过与环境 (Environment) 的交互来学习如何做出最优决策,以最大化累积奖励 (Cumulative Reward)。在交通场景中,智能体可以是交通信号控制器、自动驾驶车辆或交通管理中心,环境则是 SUMO 模拟的交通路网,奖励则与交通效率、安全、环保等目标相关。

5.1 强化学习与交通优化的契合点

交通系统本质上是动态的、随机的、高度复杂的系统,传统的基于规则或模型的优化方法在面对不断变化的需求和突发事件时,往往显得力不从心。强化学习的优势在于:

  1. 自适应性 (Adaptability):RL 智能体能够通过不断试错和学习,适应不同的交通状况和环境变化,而无需显式地对所有可能情况进行建模。
  2. 处理复杂性 (Handling Complexity):RL 可以处理高维度的状态空间和动作空间,适用于复杂的交通网络和多目标优化问题。
  3. 数据驱动决策 (Data-Driven Decisions):RL 从与环境的交互数据中学习策略,可以发现传统方法难以察觉的优化模式。
  4. 长期优化 (Long-term Optimization):RL 的目标是最大化长期累积奖励,能够学习具有远见的控制策略,而不仅仅是贪婪地优化当前状态。

在交通领域,强化学习已经被广泛应用于:

  • 自适应交通信号控制 (Adaptive Traffic Signal Control, ATSC):根据实时车流动态调整信号灯配时,减少延误,提高交叉口通行能力。
  • 匝道汇入控制 (Ramp Metering):控制车辆进入高速公路的速率,缓解主路拥堵。
  • 动态路径诱导 (Dynamic Route Guidance):为车辆提供最优路径建议,均衡路网负载。
  • 自动驾驶车辆决策 (Autonomous Vehicle Decision Making):例如换道、超车、路口通行等行为决策。
  • 车队协同控制 (Platoon Coordination):优化车队的行驶速度和间距,提高道路容量。
5.2 强化学习基础回顾

为了更好地理解后续内容,我们简要回顾一下强化学习的核心概念:

  • 智能体 (Agent):学习者和决策者。
  • 环境 (Environment):智能体所处的外部世界,与智能体交互。
  • 状态 (State, S):对环境在某一时刻的描述。例如,在交通信号控制中,状态可以是各个进口道的排队长度、当前信号相位等。
  • 动作 (Action, A):智能体可以执行的操作。例如,选择下一个绿灯相位,或者保持当前相位。
  • 奖励 (Reward, R):环境对智能体在某个状态下执行某个动作后反馈的标量信号,评价该动作的好坏。例如,负的车辆总延误、负的排队长度等。
  • 策略 (Policy, π):智能体从状态到动作的映射,即在给定状态下选择某个动作的规则或概率分布。π(a|s) = P[A_t = a | S_t = s]。
  • 价值函数 (Value Function)
    • 状态价值函数 (State-Value Function, Vπ(s)): 从状态 s 开始,遵循策略 π 能获得的期望累积奖励。
      Vπ(s) = Eπ[∑k=0 γk Rt+k+1 | St = s]
    • 动作价值函数 (Action-Value Function, Qπ(s, a)): 在状态 s 执行动作 a后,继续遵循策略 π 能获得的期望累积奖励。
      Qπ(s, a) = Eπ[∑k=0 γk Rt+k+1 | St = s, At = a]
      其中 γ (0 ≤ γ ≤ 1) 是折扣因子,表示未来奖励相对于当前奖励的重要性。
  • 马尔可夫决策过程 (Markov Decision Process, MDP):强化学习问题通常被建模为 MDP,其核心是状态具有马尔可夫性,即未来状态只依赖于当前状态和动作,与历史状态无关。
  • 探索与利用 (Exploration vs. Exploitation)
    • 利用 (Exploitation):选择当前已知的能够带来最大奖励的动作。
    • 探索 (Exploration):尝试新的动作,以发现可能更优的策略,即使这些动作当前看起来不是最优的。
      在学习过程中需要平衡这两者。
5.3 在 SUMO 中集成强化学习的通用框架

将强化学习算法应用于 SUMO 仿真进行交通优化,通常遵循以下框架:

  1. 定义强化学习问题:

    • 智能体 (Agent): 通常是 Python 脚本中实现的 RL 算法。
    • 环境 (Environment): SUMO 仿真实例。TraCI 作为智能体与 SUMO 环境交互的桥梁。
  2. 核心交互循环:

    • 步骤 1: 状态获取 (State Observation)
      • Python 脚本通过 TraCI 从 SUMO 中获取当前交通状态。
      • 这些状态信息需要被构造成 RL 智能体能够理解的形式(状态向量或张量)。
      • 例如:traci.lanearea.getJamLengthVehicle("detector_id") 获取检测器覆盖区域的排队车辆数,traci.trafficlight.getPhase("tls_id") 获取信号灯当前相位。
    • 步骤 2: 动作选择 (Action Selection)
      • RL 智能体根据当前观察到的状态和其学习到的策略,选择一个动作。
      • 对于初学者,可能会使用如 ε-greedy 策略来平衡探索和利用。
    • 步骤 3: 动作执行 (Action Execution)
      • Python 脚本通过 TraCI 将智能体选择的动作应用到 SUMO 环境中。
      • 例如:traci.trafficlight.setPhase("tls_id", phase_index) 设置信号灯的新相位。
    • 步骤 4: 环境演化与奖励计算 (Environment Step & Reward Calculation)
      • 调用 traci.simulationStep() 使 SUMO 仿真向前推进一个或多个时间步。
      • 在新的仿真步之后,计算由于上一个动作导致的奖励。奖励函数的设计至关重要,它直接引导智能体的学习方向。
      • 例如,奖励可以是负的累积车辆等待时间变化量,或者负的交叉口总排队长度。
    • 步骤 5: 智能体学习/更新 (Agent Learning/Update)
      • 智能体使用 (当前状态, 动作, 奖励, 下一状态) 这个经验元组来更新其内部模型或策略。
      • 例如,在 Q-learning 中,更新 Q 表;在 DQN 中,更新神经网络的权重。
    • 步骤 6: 循环/终止 (Loop/Termination)
      • 重复步骤 1-5,直到达到预设的训练轮次 (episodes) 或仿真时间,或者满足某个终止条件。
  3. 关键设计要素:

    • 状态表示 (State Representation):
      • 如何将 SUMO 中的原始交通数据转换为有意义且对 RL 算法有效的状态特征。
      • 需要包含足够的信息以做出明智决策,但也要避免维度灾难。
      • 常见的状态特征包括:各个进口道的排队长度、车辆密度、平均速度、当前信号相位、已持续时间等。
    • 动作空间 (Action Space):
      • 定义智能体可以执行的操作集合。
      • 对于交通信号控制:
        • 离散动作:选择下一个要切换到的信号相位。
        • 参数化动作:决定当前绿灯相位的持续时间。
      • 对于车辆控制:改变速度、换道、选择路径。
    • 奖励函数 (Reward Function Design):
      • 这是 RL 应用中最具挑战性也最关键的部分之一。
      • 奖励函数必须准确反映优化目标。
      • 例如:
        • 最小化延误:reward = -total_delayreward = previous_total_delay - current_total_delay
        • 最小化排队长度:reward = -total_queue_length
        • 最大化吞吐量:reward = number_of_vehicles_passed
        • 组合奖励:reward = w1 * (-delay) + w2 * (-queue_length) + w3 * (throughput)
      • 需要仔细设计以避免智能体学到非预期的"捷径"行为。
5.4 案例:基于 Q-learning 的自适应交通信号控制

Q-learning 是一种经典的、基于价值的、离策略 (off-policy) 的强化学习算法。它学习一个动作价值函数 Q(s, a),表示在状态 s 下执行动作 a 后能够获得的期望累积回报。

5.4.1 问题定义

  • 目标: 控制单个交叉口的交通信号灯,以最小化车辆的平均等待时间或总排队长度。
  • 环境: SUMO 模拟的单个交叉口。
  • 智能体: Q-learning 算法。

5.4.2 状态表示 (State Representation)

为了简化,我们采用离散化的状态表示。假设一个四路交叉口,每个进口道有一个主要的直行/左转相位组合。

  • 例如,可以将每个进口道的排队长度离散化为几个等级(如:0-5辆车为等级0,6-10辆车为等级1,11-15辆车为等级2,>15辆车为等级3)。
  • 当前有效的信号相位也可以作为状态的一部分。

假设有四个进口道(北N, 南S, 东E, 西W),每个进口道排队长度有 L 个等级。
如果只考虑排队长度,状态可以是 (q_N, q_S, q_E, q_W),其中 q_i 是第 i 个进口道的排队长度等级。
状态空间大小为 L4。如果再加上当前信号相位(假设有 P 个相位),则状态空间大小为 L4 * P。

示例:简化状态
为了代码演示,我们可以将状态简化为更易于管理的形式。例如,对于一个简单的十字路口,我们可以定义状态为代表各主要方向拥堵情况的元组。

假设交叉口 tls_id = "J1"
我们将定义4个相位(例如):

  • Phase 0: 南北直行绿灯 (NS_Green)
  • Phase 1: 南北左转绿灯 (NSL_Green) (如果需要)
  • Phase 2: 东西直行绿灯 (EW_Green)
  • Phase 3: 东西左转绿灯 (EWL_Green) (如果需要)

为了简化,假设我们只控制两个主相位:NS 向绿灯,EW 向绿灯。

  • 状态 (State):我们可以将每个方向(N, S, E, W)进入交叉口前一定距离内的车辆数作为状态的一部分。
    例如,状态可以定义为 (num_veh_N, num_veh_S, num_veh_E, num_veh_W, current_phase_index)
    为了使 Q 表可管理,这些车辆数需要被离散化。
    例如,0-5 辆车 -> 0, 6-10 辆车 -> 1, >10 辆车 -> 2。
    如果 current_phase_index 有2个值 (0 for NS_Green, 1 for EW_Green)。
    那么状态空间大小是 3 * 3 * 3 * 3 * 2 = 162 个状态。这对于 Q-table 来说是可行的。

5.4.3 动作空间 (Action Space)

  • 动作: 智能体在每个决策时刻选择下一个要激活的信号相位。
  • 对于我们的双相位示例:
    • 动作 0: 切换到/保持 NS_Green 相位。
    • 动作 1: 切换到/保持 EW_Green 相位。

5.4.4 奖励函数 (Reward Function)

  • 目标: 减少排队。
  • 奖励: 可以是负的当前交叉口总排队长度。
    reward = - (total_queue_length_N + total_queue_length_S + total_queue_length_E + total_queue_length_W)
    或者,奖励可以是切换前后排队长度的变化:
    reward = previous_total_queue - current_total_queue

5.4.5 Q-learning 算法

  • Q 表: 一个表格,存储每个 (状态, 动作) 对的 Q 值,Q[state][action]
  • 学习率 (Learning Rate, α): 控制新学习到的信息在多大程度上覆盖旧信息。
  • 折扣因子 (Discount Factor, γ):衡量未来奖励的重要性。
  • 探索率 (Exploration Rate, ε): 在 ε-greedy 策略中,以 ε 的概率随机选择动作(探索),以 1-ε 的概率选择当前 Q 值最大的动作(利用)。

Q 值更新规则:
当智能体从状态 s 执行动作 a,得到奖励 r,并转移到新状态 s' 时,Q 值的更新如下:
Q(s, a) ← Q(s, a) + α * [r + γ * max<sub>a'</sub>(Q(s', a')) - Q(s, a)]

5.4.6 Python + TraCI 实现 Q-learning 交通信号控制 (概念性代码)

假设我们已经配置好了 SUMO 场景 (.sumocfg 文件),其中包含一个名为 "J1" 的交叉口,以及相应的检测器来获取排队长度。

import traci
import sumolib # 用于解析网络等
import numpy as np
import random
import os
import sys

# SUMO 环境变量配置 (如果需要)
if 'SUMO_HOME' in os.environ:
    tools = os.path.join(os.environ['SUMO_HOME'], 'tools')
    sys.path.append(tools)
else:
    sys.exit("please declare environment variable 'SUMO_HOME'")

# Q-learning 参数
ALPHA = 0.1  # 学习率
GAMMA = 0.9  # 折扣因子
EPSILON_START = 1.0 # 初始探索率
EPSILON_END = 0.05 # 最终探索率
EPSILON_DECAY = 0.995 # 探索率衰减因子
NUM_EPISODES = 1000 # 训练的总轮次数
SIMULATION_STEPS_PER_EPISODE = 3600 # 每轮仿真的步数
ACTION_DURATION = 10 # 每个选择的相位至少持续10个仿真步

# 交叉口和相位信息
TLS_ID = "J1" # 你的交通灯ID
# 假设我们只有两个主要相位: 0 (NS绿灯), 1 (EW绿灯)
# 实际中需要根据你的 .net.xml 文件中的 <tlLogic> 定义来确定
# 例如,相位0可能对应 traci.trafficlight.getCompleteRedYellowGreenDefinition(TLS_ID)[0].phases[0]
# 这里简化为相位索引
PHASE_NS_GREEN_INDEX = 0 # 假设南北绿灯是程序中的第0个相位
PHASE_EW_GREEN_INDEX = 2 # 假设东西绿灯是程序中的第2个相位 (跳过黄灯)
# 确保这些索引与你的信号灯定义匹配,或者你可以定义一个更详细的相位列表
# 例如 programID = traci.trafficlight.getProgram(TLS_ID)
# phases = traci.trafficlight.getCompleteRedYellowGreenDefinition(TLS_ID)[0].phases
# green_phases_indices = [i for i, p in enumerate(phases) if 'G' in p.state or 'g' in p.state]
# 为了简单,我们直接指定动作就是切换到这两个核心相位
ACTIONS = [PHASE_NS_GREEN_INDEX, PHASE_EW_GREEN_INDEX] # 可选动作:激活NS绿灯或EW绿灯
NUM_ACTIONS = len(ACTIONS)

# 状态离散化
# 假设有4条入口道,每条道的检测器ID
# 例如: "det_N_J1_0", "det_S_J1_0", "det_E_J1_0", "det_W_J1_0"
INCOMING_LANES_DETECTORS = {
   
   
    "N": ["lane_N_in_0"], # 假设北向进口道上的检测器对应车道 lane_N_in_0
    "S": ["lane_S_in_0"], # 南向
    "E": ["lane_E_in_0"], # 东向
    "W": ["lane_W_in_0"]  # 西向
}
# 实际应用中,检测器通常是 e1Detector 或 e2Detector (lane area detector)
# 这里用 getLaneIDList 获取车道,再用 getLastStepVehicleNumber 获取车道上的车辆数作为简化队列
# 或者使用 traci.lanearea.getJamLengthVehicle() 如果定义了 laneAreaDetector

MAX_CARS_PER_LANE_STATE = 3 # 离散化等级: 0 (0-5), 1 (6-10), 2 (>10)

# Q 表初始化
# 状态: (q_N, q_S, q_E, q_W, current_phase_logic_index)
# current_phase_logic_index: 0 for NS, 1 for EW (代表当前哪个方向是绿灯)
# q_N, q_S, q_E, q_W 的取值范围是 0, 1, 2
# current_phase_logic_index 的取值范围是 0, 1
q_table = np.zeros((MAX_CARS_PER_LANE_STATE, MAX_CARS_PER_LANE_STATE,
                    MAX_CARS_PER_LANE_STATE, MAX_CARS_PER_LANE_STATE,
                    len(ACTIONS), # 代表当前是哪个主要流向的绿灯
                    NUM_ACTIONS)) # Q(s,a)

def get_sumo_executable(gui=False):
    """获取SUMO或SUMO-GUI的可执行文件路径"""
    if gui:
        return "sumo-gui" # 返回SUMO GUI的可执行文件名
    return "sumo" # 返回SUMO命令行版本的可执行文件名

def start_simulation(sumocfg_file, gui=False):
    """启动SUMO仿真"""
    sumo_cmd = [get_sumo_executable(gui), "-c", sumocfg_file] # 构建SUMO启动命令
    sumo_cmd.extend(["--step-length", "1"]) # 设置仿真步长为1秒
    sumo_cmd.extend(["--remote-port", "8813"]) # 指定TraCI端口
    sumo_cmd.extend(["--waiting-time-memory", "1000"]) # 增加车辆等待时间记忆长度,用于获取准确的累计等待时间
    # sumo_cmd.extend(["--time-to-teleport", "-1"]) # 防止车辆瞬移,让拥堵更真实
    traci.start(sumo_cmd, port=8813) # 启动SUMO并通过TraCI连接
    print(f"SUMO started with command: {
     
     ' '.join(sumo_cmd)}") # 打印启动命令

def get_lane_queue_length(lane_id):
    """获取指定车道的排队车辆数 (简化版,实际中用检测器更佳)"""
    # 这是一个非常简化的版本,实际中应该用检测器
    # 例如 traci.lanearea.getJamLengthVehicle(detector_id) 或 traci.edge.getLastStepHaltingNumber(edge_id)
    # 这里我们用 traci.lane.getLastStepHaltingNumber(lane_id)
    try:
        return traci.lane.getLastStepHaltingNumber(lane_id) # 获取车道上一步的停止车辆数
    except traci.TraCIException:
        print(f"Warning: Could not get halting number for lane {
     
     lane_id}") # 打印警告信息
        return 0 # 返回0,如果获取失败

def discretize_queue(queue_length):
    """将排队长度离散化"""
    if queue_length <= 5: # 如果排队长度小于等于5
        return 0 # 返回离散状态0
    elif queue_length <= 10: # 如果排队长度小于等于10
        return 1 # 返回离散状态1
    else: # 其他情况
        return 2 # 返回离散状态2

def get_state():
    """从SUMO获取当前状态并离散化"""
    queues = [] # 初始化队列列表
    for direction_lanes in INCOMING_LANES_DETECTORS.values(): # 遍历所有方向的车道
        direction_queue = 0 # 初始化方向队列长度
        for lane_id in direction_lanes: # 遍历该方向的所有车道
            direction_queue += get_lane_queue_length(lane_id) # 累加车道队列长度
        queues.append(discretize_queue(direction_queue)) # 将离散化后的队列长度添加到列表

    # 获取当前信号灯相位逻辑 (哪个主要方向是绿灯)
    # 我们需要将实际的相位索引映射到我们定义的简化逻辑索引 (0 for NS, 1 for EW)
    current_sumo_phase = traci.trafficlight.getPhase(TLS_ID) # 获取当前SUMO相位索引
    
    # 这个映射逻辑非常重要,需要根据你的信号灯定义来确定
    # 假设 PHASE_NS_GREEN_INDEX (e.g., 0) 对应我们的逻辑状态 0
    # 假设 PHASE_EW_GREEN_INDEX (e.g., 2) 对应我们的逻辑状态 1
    # 其他黄灯、红灯相位,我们需要判断它们之前是哪个绿灯相位
    # 这是一个简化逻辑,实际中可能需要更复杂的判断或记录上一个绿灯相位
    # 为了简单,我们直接判断当前相位是否是我们定义的两个主绿灯相位之一
    current_phase_logic_index = 0 # 默认为NS方向绿灯的逻辑索引
    if current_sumo_phase == PHASE_EW_GREEN_INDEX: # 如果当前SUMO相位是东西绿灯
        current_phase_logic_index = 1 # 设置逻辑索引为1 (EW)
    elif current_sumo_phase == PHASE_NS_GREEN_INDEX: # 如果当前SUMO相位是南北绿灯
        current_phase_logic_index = 0 # 设置逻辑索引为0 (NS)
    else:
        # 如果是黄灯或全红,我们可能需要知道它从哪个绿灯转换而来
        # 为了简化,如果不是明确的NS或EW绿灯,我们可能基于上一个动作或一个默认值
        # 这里我们先假设它保持上一个逻辑状态,或者需要一个全局变量来跟踪
        # 在这个例子中,我们期望动作总是切换到明确的NS绿灯或EW绿灯之一
        # 如果是黄灯,它通常属于前一个绿灯阶段的延续。
        # 更鲁棒的方法是记录上一个 chosen_action (代表的逻辑相位)
        # 这里我们尝试从 traci.trafficlight.getProgram(TLS_ID) 和 traci.trafficlight.getPhase(TLS_ID)
        # 来推断当前是哪个方向的绿灯亮起。
        # 对于Q-learning,状态定义需要稳定。
        # 假设我们的动作直接设定到 NS_GREEN 或 EW_GREEN, 黄灯是过渡
        # 那么 current_phase_logic_index 可以反映当前哪个方向 *应该* 是绿灯
        # 或者我们可以更简单地,将当前实际的SUMO相位索引作为状态的一部分,
        # 但这会增加状态空间的维度。
        # 此处的简化:如果不是我们关心的两个主绿灯,就看上一次的动作是哪个。
        # 为了Q-table的索引,这个current_phase_logic_index必须是0或1.
        # 我们假设动作执行后,相位会变成我们期望的主绿灯相位之一
        # 所以,在get_state()时,这个值反映了*当前*哪个主要方向是绿灯。
        # 这个逻辑需要根据实际的信号灯配置来精确化。
        # 例如,如果你的相位0是NS绿,相位1是NS黄,相位2是EW绿,相位3是EW黄
        # 当 current_sumo_phase 是 0 或 1 时,逻辑上是NS绿。
        # 当 current_sumo_phase 是 2 或 3 时,逻辑上是EW绿。
        # 在这个示例中,我们之前定义了ACTIONS = [PHASE_NS_GREEN_INDEX, PHASE_EW_GREEN_INDEX]
        # 所以,如果当前相位是 PHASE_NS_GREEN_INDEX, 我们的逻辑相位就是0
        # 如果当前相位是 PHASE_EW_GREEN_INDEX, 我们的逻辑相位就是1
        # 如果是黄灯,它属于前一个绿灯。
        # traci.trafficlight.getPhase(TLS_ID) 返回的是程序中的绝对相位索引
        program_logic = traci.trafficlight.getCompleteRedYellowGreenDefinition(TLS_ID)[0] # 获取当前程序逻辑
        current_phase_definition = program_logic.phases[current_sumo_phase].state # 获取当前相位的灯色字符串
        
        # 一个更可靠的方法是检查当前相位定义中哪个方向是绿灯
        # 例如,如果 'G' 或 'g' 出现在代表南北向的灯位上
        # 但这依赖于灯色字符串的复杂解析。
        # 简化:如果当前是南北绿,phase_logic_index = 0, 如果是东西绿,则为 1
        # 黄灯阶段通常意味着前一个绿灯阶段的结束。
        # 为了Q-table索引,我们需要一个明确的逻辑状态。
        # 假设我们总是从一个绿灯相位切换到另一个绿灯相位(中间有黄灯)
        # state 中的 current_phase_logic_index 代表“哪个方向刚刚或正在经历绿灯”
        # 如果 traci.trafficlight.getPhase(TLS_ID) 是 PHASE_NS_GREEN_INDEX -> 0
        # 如果 traci.trafficlight.getPhase(TLS_ID) 是 PHASE_EW_GREEN_INDEX -> 1
        # 如果是黄灯,它紧随绿灯之后。
        # 这个state的 current_phase_logic_index 代表上一个或当前的主要绿灯方向。
        # 更好的做法可能是,这个状态元素代表“即将为哪个方向开绿灯的决策”已做出,
        # 或者直接使用上一个动作作为状态的一部分。

        # 为了Q-table的稳定,我们假设 current_phase_logic_index 代表当前 *主要* 的绿灯方向
        # 如果当前是黄灯,它通常指示着从哪个绿灯相位转换而来。
        # e.g. if current_sumo_phase is yellow after NS_GREEN, logic is still NS (0)
        # 为了简单,我们假设动作执行后,相位会变成我们期望的主绿灯相位之一
        # 然后我们才获取状态。所以这里应该反映的是已设置的绿灯。
        # 实际中,RL的状态需要精确定义。
        # 让我们假设一个全局变量 `last_chosen_logic_phase` 来记录上一个选择的逻辑相位
        # 并在动作执行后更新它。
        # state_tuple = tuple(queues + [last_chosen_logic_phase]) # 拼接成元组作为状态
        # 另一种方式:直接从当前相位判断。
        # 假设 phase 0, 1 是 NS (绿,黄),phase 2, 3 是 EW (绿,黄)
        # if current_sumo_phase in [0, 1]: current_phase_logic_index = 0
        # elif current_sumo_phase in [2, 3]: current_phase_logic_index = 1
        # 这个需要根据你的 `TLS_ID` 的具体相位定义来确定。
        # 对于本例,我们直接使用我们定义的 `ACTIONS` 列表中的索引。
        # 如果当前相位是ACTIONS[0] (PHASE_NS_GREEN_INDEX),则逻辑相位为0.
        # 如果当前相位是ACTIONS[1] (PHASE_EW_GREEN_INDEX),则逻辑相位为1.
        # 黄灯阶段较为复杂,它通常与前一个绿灯相位关联。
        # 为了Q表查找,这个逻辑索引必须是确定的。
        # 我们假设状态是在“绿灯”期间或即将进入绿灯时评估的。
        # 这里的 current_phase_logic_index 指的是当前哪个 *逻辑* 方向是绿灯。
        # 0 代表南北,1 代表东西。
        active_program_id = traci.trafficlight.getProgram(TLS_ID) # 获取当前活动的程序ID
        all_logics = traci.trafficlight.getAllProgramLogics() # 获取所有程序逻辑
        current_logic = None # 初始化当前逻辑
        for logic in all_logics: # 遍历所有逻辑
            if logic.programID == active_program_id: # 如果找到当前活动的程序ID
                current_logic = logic # 设置为当前逻辑
                break # 跳出循环
        
        if current_logic: # 如果找到了当前逻辑
            # 假设 PHASE_NS_GREEN_INDEX 是南北绿灯相位在当前 program 中的索引
            # 假设 PHASE_EW_GREEN_INDEX 是东西绿灯相位在当前 program 中的索引
            # 我们需要判断当前相位 `current_sumo_phase` 属于哪个逻辑方向。
            # 例如,如果 `current_sumo_phase` 等于我们定义的南北绿灯相位索引,
            # 或者与南北绿灯相关联的黄灯相位索引。
            # 这是一个困难点,因为SUMO的相位索引是绝对的,而我们的逻辑相位是抽象的。
            # 简化:我们假设 `last_action_logic_index` 是一个全局变量,在执行动作后更新。
            # `state = tuple(queues + [last_action_logic_index])`
            # 为避免引入全局变量,我们尝试从当前相位推断。
            # 如果当前相位是`PHASE_NS_GREEN_INDEX`,则逻辑相位为0。
            # 如果当前相位是`PHASE_EW_GREEN_INDEX`,则逻辑相位为1。
            # 对于黄灯,它属于前一个绿灯。
            # 让我们用一个更简单的状态,只基于队列。
            # 并且,当前相位信息由Q-table的结构隐式处理,或者我们让动作决定下一个相位,
            # 而不是将当前相位作为状态的一部分。
            # 如果动作是“切换到NS”或“切换到EW”,那么当前相位信息可能不是那么重要了,
            # 因为Q值是 Q(s, a),其中a已经是决策。
            # 让我们暂时移除 current_phase_logic_index from state for simplification of Q-table,
            # and make the Q-table Q[q_N][q_S][q_E][q_W][action_index]
            # q_table = np.zeros((MAX_CARS_PER_LANE_STATE, ..., NUM_ACTIONS))
            # 但是标准的Q-learning是 Q(s,a),所以状态中包含当前相位通常是必要的。
            
            # 重新考虑:state 中包含当前哪个 *主要方向* 是绿灯。
            # 0 for NS, 1 for EW.
            # 这个映射需要你根据自己的 .net.xml 中的 <tlLogic> 定义。
            # e.g. if phases 0,1,2 are for NS (G, y, G_ped), and 3,4,5 for EW (G,y,G_ped)
            # then if current_sumo_phase in [0,1,2], current_phase_logic_index = 0
            # else current_phase_logic_index = 1
            # 为了本代码的通用性,我们假设一个函数 `get_current_logic_phase_index(current_sumo_phase)`
            # 在这里我们硬编码一个简单的映射,假设PHASE_NS_GREEN_INDEX和相关的黄灯属于逻辑0,
            # PHASE_EW_GREEN_INDEX和相关的黄灯属于逻辑1。
            # 假设黄灯相位紧随绿灯相位。
            # if current_sumo_phase == PHASE_NS_GREEN_INDEX or current_sumo_phase == PHASE_NS_GREEN_INDEX + 1:
            # current_phase_logic_index = 0
            # elif current_sumo_phase == PHASE_EW_GREEN_INDEX or current_sumo_phase == PHASE_EW_GREEN_INDEX + 1:
            # current_phase_logic_index = 1
            # 这个逻辑需要用户根据自己的信号灯定义来适配。
            # 我们将假设 `current_phase_logic_index` 来源于上一次执行的动作的意图。
            # 或者,更简单,我们直接用当前SUMO相位模除一个基数(如果相位有规律)
            # 为了这个例子,我们将使用一个全局变量来跟踪上一个选择的逻辑相位。
            global last_selected_logic_phase_index # 声明使用全局变量
            state_tuple = tuple(queues + [last_selected_logic_phase_index]) # 组成状态元组
            return state_tuple # 返回状态元组
    return tuple([0]*len(INCOMING_LANES_DETECTORS) + [0]) # 如果无法获取逻辑,返回默认状态


def get_reward(current_queues_raw):
    """计算奖励,负的当前总排队长度"""
    # current_queues_raw 是一个包含各方向原始车辆数的列表或元组
    return -sum(current_queues_raw) # 返回负的总排队长度

def choose_action(state, epsilon):
    """使用epsilon-greedy策略选择动作"""
    if random.random() < epsilon: # 如果随机数小于epsilon
        return random.choice(range(NUM_ACTIONS)) # 随机选择一个动作 (探索)
    else: # 否则
        # state是一个元组 (qN, qS, qE, qW, current_phase_logic_idx)
        # q_table[state] 会因为 state 是元组而直接索引
        return np.argmax(q_table[state]) # 选择Q值最大的动作 (利用)

def run_episode(episode_num, current_epsilon, sumocfg_file, gui=False):
    """运行一个训练轮次"""
    global last_selected_logic_phase_index # 声明使用全局变量
    start_simulation(sumocfg_file, gui=gui) # 启动仿真
    
    # 初始设置一个默认的逻辑相位,例如南北绿灯
    last_selected_logic_phase_index = 0 # 假设初始是南北绿灯逻辑
    traci.trafficlight.setPhase(TLS_ID, ACTIONS[last_selected_logic_phase_index]) # 设置初始相位
    
    # 为了获取初始状态,先运行几步让交通流稳定一下,并应用初始相位
    for _ in range(5): # 运行5个仿真步
        traci.simulationStep() # 执行一步仿真

    current_state = get_state() # 获取初始状态
    total_reward_this_episode = 0 # 初始化本轮总奖励

    # 为了计算奖励,我们需要未离散化的队列长度
    raw_queues_for_reward = [] # 初始化原始队列长度列表
    for direction_lanes in INCOMING_LANES_DETECTORS.values(): # 遍历所有方向的车道
        direction_queue = 0 # 初始化方向队列长度
        for lane_id in direction_lanes: # 遍历该方向所有车道
            direction_queue += get_lane_queue_length(lane_id) # 累加车道队列长度
        raw_queues_for_reward.append(direction_queue) # 添加到原始队列列表

    simulation_time = 0 # 初始化仿真时间
    while simulation_time < SIMULATION_STEPS_PER_EPISODE: # 当仿真时间小于每轮最大步数
        action_logic_index = choose_action(current_state, current_epsilon) # 选择动作(逻辑索引 0 或 1)
        chosen_sumo_phase = ACTIONS[action_logic_index] # 将逻辑动作索引映射到SUMO相位索引

        traci.trafficlight.setPhase(TLS_ID, chosen_sumo_phase) # 应用选择的SUMO相位
        last_selected_logic_phase_index = action_logic_index # 更新上一个选择的逻辑相位索引

        # 让选定的相位持续一段时间
        for _ in range(ACTION_DURATION): # 循环相位持续时间
            if simulation_time >= SIMULATION_STEPS_PER_EPISODE: # 如果达到最大步数
                break # 跳出循环
            traci.simulationStep() # 执行一步仿真
            simulation_time += 1 # 仿真时间加1
        
        if simulation_time >= SIMULATION_STEPS_PER_EPISODE: # 再次检查是否达到最大步数
             # 在结束前获取最后的状态和奖励
            next_raw_queues_for_reward = [] # 初始化下一个原始队列长度列表
            for direction_lanes in INCOMING_LANES_DETECTORS.values(): # 遍历所有方向的车道
                direction_queue = 0 # 初始化方向队列长度
                for lane_id in direction_lanes: # 遍历该方向所有车道
                    direction_queue += get_lane_queue_length(lane_id) # 累加车道队列长度
                next_raw_queues_for_reward.append(direction_queue) # 添加到下一个原始队列列表
            
            reward = get_reward(next_raw_queues_for_reward) # 计算奖励
            next_state = get_state() # 获取下一个状态(虽然可能不会用于更新,因为是episode末尾)
            break # 结束循环

        next_raw_queues_for_reward = [] # 初始化下一个原始队列长度列表
        for direction_lanes in INCOMING_LANES_DETECTORS.values(): # 遍历所有方向的车道
            direction_queue = 0 # 初始化方向队列长度
            for lane_id in direction_lanes: # 遍历该方向所有车道
                direction_queue += get_lane_queue_length(lane_id) # 累加车道队列长度
            next_raw_queues_for_reward.append(direction_queue) # 添加到下一个原始队列列表
        
        reward = get_reward(next_raw_queues_for_reward) # 计算奖励
        next_state = get_state() # 获取下一个状态

        # Q-table 更新
        # current_state 是一个元组,可以直接用作q_table的索引
        # action_logic_index 是选择的动作索引 (0 或 1)
        best_next_action = np.argmax(q_table[next_state]) # 找到下一个状态下Q值最大的动作
        td_target = reward + GAMMA * q_table[next_state][best_next_action] # 计算TD目标值
        td_delta = td_target - q_table[current_state][action_logic_index] # 计算TD误差
        q_table[current_state][action_logic_index] += ALPHA * td_delta # 更新Q值

        current_state = next_state # 更新当前状态
        raw_queues_for_reward = next_raw_queues_for_reward # 更新原始队列长度
        total_reward_this_episode += reward # 累加奖励

    traci.close() # 关闭TraCI连接
    print(f"Episode {
     
     episode_num + 1}: Total Reward: {
     
     total_reward_this_episode}, Epsilon: {
     
     current_epsilon:.3f}") # 打印轮次信息
    return total_reward_this_episode # 返回本轮总奖励

# ---- 全局变量,用于 get_state ----
# 这个全局变量的引入是为了简化状态表示中“当前逻辑相位”的获取。
# 在更复杂的实现中,这部分状态可以从历史动作或者对SUMO相位的详细解析中得到。
last_selected_logic_phase_index = 0 # 初始化为0 (例如,代表NS绿灯逻辑)

# 主训练循环
if __name__ == '__main__':
    sumocfg_file_path = "your_scenario.sumocfg" # 指定你的SUMO配置文件路径
    # 请确保 "your_scenario.sumocfg" 文件存在,并且包含名为 "J1" 的交通灯
    # 以及在 INCOMING_LANES_DETECTORS 中定义的车道
    # 并且交通灯 "J1" 的程序中,相位 ACTIONS[0] 和 ACTIONS[1] 是有效的绿灯相位。

    # 示例:创建一个简单的SUMO场景文件(如果它们不存在)
    # 为了能运行,你需要一个最基本的 .net.xml, .rou.xml, .sumocfg
    # network_file = "rl_intersection.net.xml"
    # route_file = "rl_routes.rou.xml"
    # config_file = "rl_config.sumocfg"
    #
    # if not os.path.exists(network_file):
    #     with open(network_file, "w") as f:
    #         f.write("""<network>
    # <node id="J1" x="0.0" y="0.0" type="traffic_light"/>
    # <node id="N1" x="0.0" y="100.0" type="priority"/>
    # <node id="S1" x="0.0" y="-100.0" type="priority"/>
    # <node id="E1" x="100.0" y="0.0" type="priority"/>
    # <node id="W1" x="-100.0" y="0.0" type="priority"/>
    #
    # <edge id="N_in" from="N1" to="J1" priority="1">
    #    <lane id="lane_N_in_0" index="0" speed="13.89" length="100.0" shape="0.0,95.0 0.0,5.0"/>
    # </edge>
    # <edge id="S_in" from="S1" to="J1" priority="1">
    #    <lane id="lane_S_in_0" index="0" speed="13.89" length="100.0" shape="0.0,-95.0 0.0,-5.0"/>
    # </edge>
    # <edge id="E_in" from="E1" to="J1" priority="1">
    #    <lane id="lane_E_in_0" index="0" speed="13.89" length="100.0" shape="95.0,0.0 5.0,0.0"/>
    # </edge>
    # <edge id="W_in" from="W1" to="J1" priority="1">
    #    <lane id="lane_W_in_0" index="0" speed="13.89" length="100.0" shape="-95.0,0.0 -5.0,0.0"/>
    # </edge>
    #
    # <edge id="out_N" from="J1" to="N1" priority="1"/>
    # <edge id="out_S" from="J1" to="S1" priority="1"/>
    # <edge id="out_E" from="J1" to="E1" priority="1"/>
    # <edge id="out_W" from="J1" to="W1" priority="1"/>
    #
    # <tlLogic id="J1" type="static" programID="0" offset="0">
    #    <phase duration="30" state="GrGr"/> <!-- NS Green (Incorrect, should be e.g. GGrrGGrr for 4 approaches) -->
    #    <phase duration="5"  state="yryr"/> <!-- NS Yellow -->
    #    <phase duration="30" state="rGrG"/> <!-- EW Green -->
    #    <phase duration="5"  state="ryry"/> <!-- EW Yellow -->
    # </tlLogic>
    # <!-- A more correct phase definition for a 4-arm intersection:
    # J1 has incoming edges: E_in, N_in, S_in, W_in (order matters for 'state' string)
    # Connections:
    # E_in -> out_W, out_N, out_S
    # N_in -> out_S, out_E, out_W
    # S_in -> out_N, out_E, out_W
    # W_in -> out_E, out_N, out_S
    # Assuming standard 12-signal group (3 per approach: Left, Through, Right)
    # State string: E_L, E_T, E_R, N_L, N_T, N_R, S_L, S_T, S_R, W_L, W_T, W_R
    # Example state string for NS Green (N-S through, N-S Right, S-N Right):
    # Phase 0 (NS Green): "rrgGgrrgGgrr" (assuming E,N,S,W order for approaches in TL definition)
    # Phase 1 (NS Yellow): "rryygrryygrr"
    # Phase 2 (EW Green): "GgrrGgrrggGG"
    # Phase 3 (EW Yellow): "yyrryyrrggYY"
    # The PHASE_NS_GREEN_INDEX and PHASE_EW_GREEN_INDEX must match these.
    # For this example code, let's assume a simplified phase definition:
    # <tlLogic id="J1" type="static" programID="0" offset="0">
    #    <phase duration="30" state="GGggrrrrGGgg"/> <!-- NS Green (index 0) -->
    #    <phase duration="4"  state="yyyyrrrryyyy"/> <!-- NS Yellow (index 1) -->
    #    <phase duration="30" state="rrrrGGggrrrr"/> <!-- EW Green (index 2) -->
    #    <phase duration="4"  state="rrrryyyyrrrr"/> <!-- EW Yellow (index 3) -->
    # </tlLogic>
    # Then PHASE_NS_GREEN_INDEX = 0, PHASE_EW_GREEN_INDEX = 2
    # The `state` attribute in <phase> refers to the signal states for all links controlled by the TLS.
    # The order of characters in `state` corresponds to the order of <connection> elements for that TLS in the .net.xml file
    # OR, if `tlType="static"` and no connections specified, it's based on incoming lane order.
    # It's CRUCIAL that ACTIONS[0] and ACTIONS[1] correctly set the desired green p
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宅男很神经

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值