返回代码索引

env_core.py

当前主实验物理过程、观测、event、覆盖与奖励

原文件 · 2389 行 · 110706 字节

envs/delivcoal/env_core.py
SHA-256: 0012ff2bfea81808b50384d8f915c9c251ad63f81c5f60b290d0e2daca0d9543

1import copy2import numpy as np3import pickle4import os5from collections import deque6import yaml78eps = 1e-6910from enum import IntEnum1112class AgentType(IntEnum):13    Drone = 014    Vehicle = 11516class TaskType(IntEnum):17    Explore = 018    Deliver = 119    Idle = 220    Resupply = 32122def dist_point_segment(p, a, b):23    # 点到线段的最短距离;用于判断路径线段是否穿过感知范围或危险圆。24    p = np.array(p, dtype=float)25    a = np.array(a, dtype=float)26    b = np.array(b, dtype=float)27    ab = b - a28    if np.allclose(ab, 0):29        return np.linalg.norm(p - a)30    t = np.dot(p - a, ab) / np.dot(ab, ab)31    t = np.clip(t, 0.0, 1.0)32    proj = a + t * ab33    return np.linalg.norm(p - proj)3435def fast_path(start, goal, danger_points=None, danger_radius=1.0, clearance=0.5, max_iter=8):36    '''连续空间近似路径规划器。3738    先尝试直线到达目标;如果路径线段穿过危险圆,则在障碍物附近插入垂直偏移路点绕行。39    返回值包含起点和终点的路点列表。40    '''41    if danger_points is None:42        danger_points = []4344    def segment_intersects_circle(a, b, c, r):45        return dist_point_segment(c, a, b) <= r4647    s = tuple(map(float, start))48    g = tuple(map(float, goal))49    if np.allclose(s, g):50        return [s]5152    waypoints = [s, g]53    iter_count = 054    # 逐段检查路径;某段碰到危险圆时,就在该段中插入一个绕行点。55    while iter_count < max_iter:56        changed = False57        new_waypoints = [waypoints[0]]58        for i in range(len(waypoints)-1):59            a = waypoints[i]60            b = waypoints[i+1]61            # 只处理第一个相交障碍,插入路点后下一轮再继续检查。62            hit = None63            for c in danger_points:64                if segment_intersects_circle(a, b, c, danger_radius + clearance):65                    hit = c66                    break67            if hit is None:68                new_waypoints.append(b)69                continue70            # 绕行点取“障碍物到线段最近点”的垂直方向偏移。71            a_np = np.array(a); b_np = np.array(b); c_np = np.array(hit)72            ab = b_np - a_np73            if np.allclose(ab, 0):74                # 线段退化时没有稳定垂线方向,直接使用一个固定侧向偏移。75                dir_perp = np.array([1.0, 0.0])76            else:77                t = np.dot(c_np - a_np, ab) / np.dot(ab, ab)78                t = np.clip(t, 0.0, 1.0)79                closest = a_np + t * ab80                # 正常线段使用垂直方向生成绕行候选点。81                dir_perp = np.array([-ab[1], ab[0]])82                if np.allclose(dir_perp, 0):83                    dir_perp = np.array([1.0, 0.0])84                dir_perp = dir_perp / np.linalg.norm(dir_perp)85            # 通过比较两侧候选点离其他危险圆的距离,选择更安全的一侧。86            # 左右两侧各给一个候选绕行点,选择碰撞风险较小的一侧。87            det_dist = danger_radius + clearance88            cand1 = tuple((closest + dir_perp * det_dist).tolist()) if 'closest' in locals() else tuple((a_np + dir_perp * det_dist).tolist())89            cand2 = tuple((closest - dir_perp * det_dist).tolist()) if 'closest' in locals() else tuple((a_np - dir_perp * det_dist).tolist())90            # 分数越小表示越不容易贴近其他危险圆。91            def collision_score(pt):92                ssum = 0.093                for c2 in danger_points:94                    d = np.linalg.norm(np.array(pt) - np.array(c2)) - danger_radius95                    ssum += max(0.0, (clearance - d))96                return ssum9798            s1 = collision_score(cand1)99            s2 = collision_score(cand2)100            detour = cand1 if s1 <= s2 else cand2101            # 在当前线段终点前插入绕行点。102            new_waypoints.append(detour)103            new_waypoints.append(b)104            changed = True105        waypoints = new_waypoints106        iter_count += 1107        if not changed:108            break109110    # 去掉几乎重合的路点,避免移动时产生零长度线段。111    out = [tuple(map(float, waypoints[0]))]112    for p in waypoints[1:]:113        if np.linalg.norm(np.array(p) - np.array(out[-1])) > eps:114            out.append(tuple(map(float, p)))115    return out116117class AgentLastState:118    """保存上一步智能体状态,奖励函数用它计算切换、距离变化和资源变化。"""119120    __slots__ = ['coalition', 'new_task_found', 'current_task_completed', 'any_task_completed', 'coalition_completed', 'dist', 'load', 'idle']121    122    def __init__(self):123        self.coalition = EnvCore.idle_task_id124        self.new_task_found = False125        self.current_task_completed = False126        self.any_task_completed = False127        self.coalition_completed = False128        self.dist = 0.0129        self.load = 0.0130        self.idle = True131132class Agent:133    """环境内部的智能体实体。134135    capbilities 四维分别驱动感知、移动、载荷和电量上限;每步事件字段在 step 末尾清零。136    """137138    __slots__ = ['aid', 'position', 'capbilities', 'perception', 'mobility', 'load',139                 'battery', 'type', 'living', 'idle', 'coalition', 'path', 'last_state',140                 'dead', 'new_task_found', 'coalition_changed', 'discovery', 'delivery', 'coalition_delivery', 'resupply_ratio', 'init_load', 'resupplied',141                 'current_task_completed', 'any_task_completed', 'coalition_completed', 'dist', 'unchanged_time',142                 'idle_wait_steps']143144    def __init__(self, aid: int, pos: np.ndarray, capbilities: np.ndarray, rng=None):145        self.aid = aid146        self.position = np.array(pos, dtype=float)147        self.capbilities = capbilities148        self.perception = float(capbilities[0] * 50.0)149        self.mobility = float(capbilities[1] * 10) + 10150        self.init_load = int(capbilities[2] * 70) + 30151        self.load = self.init_load152        self.battery = int(capbilities[3] * 500) + 500153        self.type = rng.choice([AgentType.Drone, AgentType.Vehicle]) if rng is not None else np.random.choice([AgentType.Drone, AgentType.Vehicle])154        self.living = True155        self.idle = True156        self.coalition = EnvCore.idle_task_id157        self.path = []158159        self.dead = False160        self.new_task_found = False161        self.current_task_completed = False162        self.any_task_completed = False163        self.coalition_completed = False164        self.coalition_changed = False165        self.discovery = 0.0166        self.delivery = 0.0167        self.coalition_delivery = 0.0168        self.dist = 0.0169        self.unchanged_time = 0.0170        self.idle_wait_steps = 0171172        self.resupply_ratio = 0.0173        self.resupplied = False174175        self.last_state = AgentLastState()176177    def act(self, cid: int):178        # 只有联盟编号变化时才清空路径;保持同一联盟会累积 unchanged_time。179        if cid != self.coalition:180            self.coalition = cid181            self.coalition_changed = True182            self.path = []183            self.unchanged_time = 0.0184        else:185            self.unchanged_time += 1.0186187        if cid == EnvCore.idle_task_id:188            self.idle = True189        else:190            self.idle_wait_steps = 0191192    def consume(self):193        # 当前模型按步扣电,电量耗尽后变为死亡,死亡事件只在本步奖励中使用。194        if self.living:195            self.battery -= 1196            if self.battery == 0:197                self.living = False198                self.dead = True199200    def deliver(self, demand):201        # 交付量受当前载荷和任务剩余需求共同限制;无货交付会给负事件信号。202        delivery = np.min([self.load, demand])203        self.load -= delivery204        self.delivery = delivery if delivery > 0 else -10205        self.idle = True206207        if self.delivery < 0:208            pass209            210        return delivery211212    def complete(self):213        # 当前智能体完成自己负责的当前动作,runner 会据此决定是否再次激活该 agent。214        self.current_task_completed = True215        self.idle = True216217    def resupply(self):218        # 补给恢复电量和载荷,并记录本次补给比例用于奖励。219        self.living = True220        self.dead = False221        self.idle = True222        self.battery = int(self.capbilities[3] * 500) + 500223        self.resupply_ratio = (self.init_load - self.load) / self.init_load224        self.resupplied = True225        self.load = self.init_load226227    def step(self):228        # 先把当前状态拷贝到 last_state,再清空“本步事件”字段。229        # 这样奖励函数可以同时访问上一步状态和本步事件。230        for key in self.last_state.__slots__:231            attr = getattr(self, key)232            setattr(self.last_state, key, attr)233234        self.dead = False235        self.new_task_found = False236        self.current_task_completed = False237        self.any_task_completed = False238        self.coalition_completed = False239        self.coalition_changed = False240        self.discovery = 0.0241        self.delivery = 0.0242        self.coalition_delivery = 0.0243        self.resupply_ratio = 0.0244        self.resupplied = False245        self.idle = False246247248class Task:249    """环境内部任务实体。250251    Deliver 任务初始不可见;Explore/Idle/Resupply 是辅助任务槽,始终可见。252    """253254    __slots__ = [255        'id', 'cap_dim', 'position', 'demand', 'initial_demand', 'urgency',256        'req_vec', 'req_ratio', 'type', 'discovered', 'completed',257        'discovered_step', 'first_assigned_step', 'response_time'258    ]259260    def __init__(self, cid: int, pos: np.ndarray, demand: int, urgency: float, cap_dim: int, rng=None):261        self.id = cid262        self.cap_dim = cap_dim263        self.position = np.array(pos, dtype=float)264        self.demand = demand265        self.initial_demand = demand266        self.urgency = urgency267        self.req_vec = ((rng.rand(cap_dim) if rng is not None else np.random.rand(cap_dim)) * (demand / 100.0)).astype(float)268        self.req_ratio = np.zeros(cap_dim)269        self.type = TaskType.Deliver270        self.discovered = False271        self.completed = False272273        self.discovered_step = None274        self.first_assigned_step = None275        self.response_time = None276277    def calc_req_ratio(self, cap_now):278        # req_ratio 是当前联盟能力相对需求的比例差,只用于 info/诊断,不直接进入局部观测。279        if self.discovered and not self.completed:280            req_vec = self.req_vec281            for dim in range(self.cap_dim):282                if np.isnan(req_vec[dim]) or req_vec[dim] <= 0.0:283                    self.req_ratio[dim] = 0.0284                else:285                    self.req_ratio[dim] = (cap_now[dim] - req_vec[dim]) / req_vec[dim]286        else:287            self.req_ratio = np.zeros(self.cap_dim)288289290291class EnvCore:292    """MACF 环境核心。293294    负责连续地图、智能体/任务状态推进、局部 Dict 观测、全局 critic state、奖励和终止条件。295    Gym wrapper 只负责把这里的返回值包装成向量环境接口。296    """297298    explore_task_id = -1299    idle_task_id = -1300    resupply_task_id = -1301302    def __init__(self, config):303        # scenario_name 决定是否启用 coalition_delivery 专用任务特征和奖励。304        scenario_name = getattr(config, "scenario_name", None)305        if scenario_name in (None, "", "MyEnv", "coadel", "delivcoal"):306            scenario_name = "coalition_delivery"307        self.scenario_name = scenario_name308        self.use_coalition_delivery = self.scenario_name == "coalition_delivery"309        self.coalition_delivery_reward_mode = getattr(310            config,311            "reward_mode",312            getattr(config, "coalition_delivery_reward_mode", "baseline"),313        )314        if self.coalition_delivery_reward_mode in ("v1", "local", "local_progress"):315            self.coalition_delivery_reward_mode = "delivcoal_v1"316        elif self.coalition_delivery_reward_mode in ("v2", "local_shaped", "potential"):317            self.coalition_delivery_reward_mode = "delivcoal_v2"318        elif self.coalition_delivery_reward_mode in ("v3", "potential_small"):319            self.coalition_delivery_reward_mode = "delivcoal_v3"320        elif self.coalition_delivery_reward_mode in ("v4", "potential_medium"):321            self.coalition_delivery_reward_mode = "delivcoal_v4"322        elif self.coalition_delivery_reward_mode in ("v5", "potential_local"):323            self.coalition_delivery_reward_mode = "delivcoal_v5"324        if self.coalition_delivery_reward_mode not in (325            "baseline",326            "shaped",327            "delivcoal_v1",328            "delivcoal_v2",329            "delivcoal_v3",330            "delivcoal_v4",331            "delivcoal_v5",332        ):333            raise ValueError(334                "coalition_delivery_reward_mode must be 'baseline', 'shaped', "335                "'delivcoal_v1', 'delivcoal_v2', 'delivcoal_v3', "336                "'delivcoal_v4', or 'delivcoal_v5', "337                f"got {self.coalition_delivery_reward_mode!r}"338            )339340        deliver_target_num = config.target_num341        # 探索动作可以是单一 nearest,也可以拆成多个 sector/survey 动作槽。342        self.explore_action_mode = getattr(config, "explore_action_mode", "nearest")343        if self.explore_action_mode not in ("nearest", "sector", "survey"):344            raise ValueError(f"Unsupported explore_action_mode: {self.explore_action_mode}")345        self.num_explore_actions = (346            int(getattr(config, "num_explore_actions", 1))347            if self.explore_action_mode in ("sector", "survey")348            else 1349        )350        if self.num_explore_actions <= 0:351            raise ValueError("num_explore_actions must be positive.")352        self._configured_explore_step_size = getattr(config, "explore_step_size", None)353        self.explore_random_jitter = float(getattr(config, "explore_random_jitter", 0.0))354        self.sector_redecision_on_waypoint = bool(355            getattr(config, "sector_redecision_on_waypoint", True)356        )357        self.explore_event_redecision = bool(358            getattr(config, "explore_event_redecision", True)359        )360        self.sector_min_leg_mobility_steps = float(361            getattr(config, "sector_min_leg_mobility_steps", 1.0)362        )363        if self.sector_min_leg_mobility_steps < 0.0:364            raise ValueError("sector_min_leg_mobility_steps must be non-negative.")365        self.delivery_tasks_initially_discovered = bool(366            getattr(config, "delivery_tasks_initially_discovered", False)367        )368        self.mask_explore_actions = bool(369            getattr(config, "mask_explore_actions", False)370        )371        self.idle_time_cost_mode = str(372            getattr(config, "idle_time_cost_mode", "legacy")373        )374        if self.idle_time_cost_mode not in ("legacy", "uniform"):375            raise ValueError(376                "idle_time_cost_mode must be 'legacy' or 'uniform', got "377                f"{self.idle_time_cost_mode!r}"378            )379        self.explore_coverage_features = bool(380            getattr(config, "explore_coverage_features", False)381        )382        self.explore_observer_relative_geometry = bool(383            getattr(config, "explore_observer_relative_geometry", False)384        )385        self.explore_coverage_grid_resolution = int(386            getattr(config, "explore_coverage_grid_resolution", 20)387        )388        if self.explore_coverage_grid_resolution <= 0:389            raise ValueError(390                "explore_coverage_grid_resolution must be positive."391            )392        self.explore_task_start = deliver_target_num393        self.explore_task_end = deliver_target_num + self.num_explore_actions394        # 任务槽顺序:真实配送任务 [0, target_num),探索槽,Idle 槽,Resupply 槽。395        self.__class__.explore_task_id = self.explore_task_start396        self.__class__.idle_task_id = self.explore_task_end397        self.__class__.resupply_task_id = self.explore_task_end + 1398        # 地图是连续二维空间,危险区域用圆心 + 半径表示。399        self.map_size = float(config.map_size)             # 连续地图边长 / m400        coverage_axis = (401            np.arange(self.explore_coverage_grid_resolution, dtype=np.float64)402            + 0.5403        ) * (self.map_size / self.explore_coverage_grid_resolution)404        coverage_x, coverage_y = np.meshgrid(405            coverage_axis, coverage_axis, indexing="xy"406        )407        self._coverage_points = np.stack(408            [coverage_x.reshape(-1), coverage_y.reshape(-1)], axis=-1409        )410        self._explored_cells = np.zeros(411            len(self._coverage_points), dtype=bool412        )413        self.explore_step_size = (414            float(self._configured_explore_step_size)415            if self._configured_explore_step_size is not None416            else self.map_size * 0.25417        )418        # 危险圆几何参数;clearance 是绕行路径额外安全距离。419        self.danger_radius = float(getattr(config, 'danger_radius', 1.0))420        self.clearance = float(getattr(config, 'clearance', 0.5))421        # 仍保留 UAV/UGV 数量字段;总智能体数由二者相加得到。422        self.uav_num = config.num_uav               # 无人机数量423        self.ugv_num = config.num_ugv               # 无人车数量424        self.uav_perception_rg = tuple(config.uav_perception_rg[0:2])425        self.ugv_perception_rg = tuple(config.ugv_perception_rg[0:2])426        self.delivery_task_num = config.target_num427        self.task_num = config.target_num + self.num_explore_actions + 2428        self.gat_dim = config.gat_dim               # GAT 输出维度429        self.max_steps = config.max_steps           # 最大步数430        configured_time_limit = getattr(config, "episode_time_limit", None)431        self.episode_time_limit = (432            self.max_steps433            if configured_time_limit is None434            else int(configured_time_limit)435        )436        if self.episode_time_limit <= 0:437            raise ValueError("episode_time_limit must be positive or null.")438        self.cap_dim = config.cap_dim               # 能力维度439        self.reconfiguration_mode = str(getattr(config, "reconfiguration_mode", "legacy"))440        valid_reconfiguration_modes = {"legacy", "always", "periodic", "event", "urgency", "learned"}441        if self.reconfiguration_mode not in valid_reconfiguration_modes:442            raise ValueError(f"Unsupported reconfiguration_mode: {self.reconfiguration_mode!r}")443        self.reconfiguration_interval = int(getattr(config, "reconfiguration_interval", 10))444        if self.reconfiguration_interval <= 0:445            raise ValueError("reconfiguration_interval must be positive.")446        self.reconfiguration_urgency_threshold = float(447            getattr(config, "reconfiguration_urgency_threshold", 0.7)448        )449        self.reconfiguration_urgency_aggregation = str(450            getattr(config, "reconfiguration_urgency_aggregation", "mean")451        )452        if self.reconfiguration_urgency_aggregation not in ("mean", "sum"):453            raise ValueError("reconfiguration_urgency_aggregation must be 'mean' or 'sum'.")454        self.reconfiguration_switch_cost = float(455            getattr(config, "reconfiguration_switch_cost", 0.0)456        )457        if self.reconfiguration_switch_cost < 0.0:458            raise ValueError("reconfiguration_switch_cost must be non-negative.")459        self.reconfiguration_context_features = bool(460            getattr(config, "reconfiguration_context_features", False)461        )462        self.reconfiguration_context_dim = int(463            getattr(config, "reconfiguration_context_dim", 9)464        )465        # 9 维 context 是最初暴露给 learned gate 的事件/时间特征;466        # 11 维在此基础上增加 all_still 和 mandatory,用于 hierarchical gate467        # 区分“可以选择是否重配”和“环境语义要求必须重新决策”。468        if self.reconfiguration_context_features and self.reconfiguration_context_dim not in (9, 11):469            raise ValueError("delivcoal reconfiguration_context_dim must be 9 or 11.")470471        self.reward_keys = ['switch_coalition', 'dead', 'discover', 'deliver', 'resupply', 'coalition_deliver', 'coalition_complete', 'dist', 'end']472473        # 总智能体数,后续 obs/buffer/policy 都以该值为 agent 维度。474        self.agent_num = self.uav_num + self.ugv_num475476        self.step_count = 0                 # 当前步数477        # ego 观测:位置(2)、能力(cap_dim)、当前联盟编号、当前联盟类型、478        # 载荷比例、已发现配送任务比例、电量比例。479        self.ego_feat_dim = self.cap_dim + 7480        if self.use_coalition_delivery:481            self.coalition_task_feature_mode = str(482                getattr(config, "coalition_task_feature_mode", "compact")483            )484            if self.coalition_task_feature_mode not in ("compact", "full"):485                raise ValueError(486                    "coalition_task_feature_mode must be 'compact' or 'full', got "487                    f"{self.coalition_task_feature_mode!r}"488                )489            if self.coalition_task_feature_mode == "full":490                self.per_task_feat = 2 + self.cap_dim + 1 + 1 + 3 + 1491            else:492                self.per_task_feat = 2 + 1 + 1 + 1 + 1493        else:494            self.coalition_task_feature_mode = "full"495            # 默认任务观测:位置、能力需求、剩余需求、任务类型。496            self.per_task_feat = 2 + self.cap_dim + 1 + 1497        # other 观测:位置、能力、载荷、电量、联盟编号、智能体类型、存活标志。498        self.other_agent_feat_dim = 2 + self.cap_dim + 1 + 1 + 1 + 1 + 1499        self.global_agent_feat_dim = self.other_agent_feat_dim500        # critic 的全局任务状态额外暴露 discovered/completed 标志。501        self.global_task_feat_dim = self.per_task_feat + 2502        self.global_feat_dim = 1503504        # flatten 后的 actor 观测维度;Dict 观测实际仍按 ego/tasks/others 三个 key 返回。505        self.obs_dim = (506            self.ego_feat_dim507            + self.task_num * self.per_task_feat508            + self.agent_num * self.other_agent_feat_dim509        )510511        # 动作维度等于任务槽数。Idle/Resupply/Explore 都已经是任务槽,不再额外增加动作。512        self.action_dim = self.task_num513514        # 事件标志只描述当前 step 内发生的关键事件,奖励和 active_mask 会读取它们。515        self.event_new_task_found       = 0516        self.event_task_completed       = [0 for i in range(self.agent_num)]517        self.event_any_task_completed   = 0518519        self.data_dir = 'data'520521        if config.rewards_config_file:522            with open(config.rewards_config_file, 'r', encoding='utf-8') as f:523                rewards_config = yaml.safe_load(f) or {}524        525            for key, value in rewards_config.items():526                setattr(self, key, value)527528        else:529530            self.pun_change_coalition_event = 0.001531            self.pun_change_coalition = 1532            self.w_short = 2.0   # 能力不足权重(重罚)533            self.w_surplus = 0.3 # 能力过多权重(轻罚)534            self.b_short = 8.0   # 能力不足权重(重罚)535            self.b_surplus = 20.0 # 能力过多权重(轻罚)536            self.rwd_ratio_discover_task = 5537            self.rwd_ratio_discover_dp = 1.0538            self.pun_ag_dead = 10539            self.rwd_ratio_send = 1540            self.rwd_coalition_ratio_send = 0.2541            self.rwd_task_completed  = 200542            self.rwd_coalition_task_completed = 50543            self.rwd_dist_ratio = 3.0544            self.pun_time_ratio = 0.1545546        # legacy GAT 的全局图状态占位;当前 HATT/MAPPO 主要使用 Dict state。547        self.global_state = np.zeros((self.agent_num, self.task_num, self.gat_dim))548549        # reset 相关缓存:jitter 模式复用基础场景,只扰动配送任务位置。550        self._seed = None551        self.rng = None552        self._base_initialized = False553        self._base_danger_points = None554        self._base_tasks = None555        self._base_agents = None556        self.task_position_jitter = float(getattr(config, 'task_position_jitter', 0.05))557        self.env_reset_mode = getattr(config, 'env_reset_mode', 'jitter')558559    def seed(self, seed):560        # 设置种子后清空基础场景缓存,保证下一次 reset 从该种子重新生成。561        self._seed = int(seed)562        self.rng = np.random.RandomState(self._seed)563        self._base_initialized = False564        self._base_danger_points = None565        self._base_tasks = None566        self._base_agents = None567        return [self._seed]568569    def _rand(self, *args):570        if self.rng is not None:571            return self.rng.rand(*args)572        return np.random.rand(*args)573574    def _randint(self, *args, **kwargs):575        if self.rng is not None:576            return self.rng.randint(*args, **kwargs)577        return np.random.randint(*args, **kwargs)578579    def _choice(self, a):580        if self.rng is not None:581            return self.rng.choice(a)582        return np.random.choice(a)583584    def _sector_min_leg_distance(self, ag):585        return min(586            self.explore_step_size,587            max(0.0, ag.mobility * self.sector_min_leg_mobility_steps),588        )589590    def _explore_sector_nominal_goal(self, ag, sector):591        angle, _ = self._explore_sector_angle(sector)592        direction = np.array([np.cos(angle), np.sin(angle)], dtype=float)593        return np.clip(594            ag.position + direction * self.explore_step_size,595            0.0,596            self.map_size,597        )598599    def _explore_sector_goal(self, ag):600        # sector 模式下,探索任务不是固定点,而是从当前位置朝某个扇区方向推进。601        sector = int(ag.coalition - self.explore_task_start)602        sector = max(0, min(self.num_explore_actions - 1, sector))603        angle, sector_width = self._explore_sector_angle(sector)604        if self.explore_random_jitter > 0.0:605            jitter = (float(self._rand()) * 2.0 - 1.0) * self.explore_random_jitter * sector_width606            angle += jitter607        direction = np.array([np.cos(angle), np.sin(angle)], dtype=float)608        goal = ag.position + direction * self.explore_step_size609        goal = np.clip(goal, 0.0, self.map_size)610611        # Jitter may turn a nominally valid direction into a very short clipped612        # boundary leg. Fall back to the nominal sector center in that case.613        min_leg_distance = self._sector_min_leg_distance(ag)614        if np.linalg.norm(goal - ag.position) + eps < min_leg_distance:615            nominal_goal = self._explore_sector_nominal_goal(ag, sector)616            if np.linalg.norm(nominal_goal - ag.position) + eps >= min_leg_distance:617                goal = nominal_goal618        return goal619620    def _explore_sector_angle(self, sector):621        sector_width = 2.0 * np.pi / float(self.num_explore_actions)622        return sector * sector_width, sector_width623624    def _explore_sector_anchor(self, sector):625        angle, _ = self._explore_sector_angle(sector)626        direction = np.array([np.cos(angle), np.sin(angle)], dtype=float)627        center = np.array([0.5 * self.map_size, 0.5 * self.map_size], dtype=float)628        return np.clip(center + direction * (0.45 * self.map_size), 0.0, self.map_size)629630    @staticmethod631    def _point_segment_distances(points, a, b):632        points = np.asarray(points, dtype=np.float64)633        if len(points) == 0:634            return np.empty((0,), dtype=np.float64)635        a = np.asarray(a, dtype=np.float64)636        b = np.asarray(b, dtype=np.float64)637        segment = b - a638        denominator = float(np.dot(segment, segment))639        if denominator <= eps:640            return np.linalg.norm(points - a[None, :], axis=1)641        position = ((points - a[None, :]) @ segment) / denominator642        position = np.clip(position, 0.0, 1.0)643        projection = a[None, :] + position[:, None] * segment[None, :]644        return np.linalg.norm(points - projection, axis=1)645646    def _mark_explored_segment(self, agent, start, end):647        if not self.explore_coverage_features:648            return649        sensed = self._point_segment_distances(650            self._coverage_points, start, end651        ) <= float(agent.perception)652        self._explored_cells[sensed] = True653654    def _candidate_novel_coverage(self, agent, goal):655        if not self.explore_coverage_features:656            return 0.0657        sensed = self._point_segment_distances(658            self._coverage_points, agent.position, goal659        ) <= float(agent.perception)660        sensed_count = int(np.count_nonzero(sensed))661        if sensed_count <= 0:662            return 0.0663        return float(np.count_nonzero(sensed & ~self._explored_cells) / sensed_count)664665    def _reset_exploration_coverage(self):666        self._explored_cells.fill(False)667        if not self.explore_coverage_features:668            return669        for agent in self.agents:670            self._mark_explored_segment(671                agent, agent.position, agent.position672            )673674    def _survey_anchor(self, survey_idx):675        # survey 模式将地图划分成近似网格,每个探索动作对应一个固定巡查锚点。676        cols = int(np.ceil(np.sqrt(self.num_explore_actions)))677        rows = int(np.ceil(self.num_explore_actions / cols))678        col = survey_idx % cols679        row = survey_idx // cols680        x = (col + 1.0) / (cols + 1.0) * self.map_size681        y = (row + 1.0) / (rows + 1.0) * self.map_size682        return np.array([x, y], dtype=float)683684    def _is_explore_task_id(self, task_id):685        # 判断某个任务槽是否属于探索动作槽。686        return self.explore_task_start <= int(task_id) < self.explore_task_end687688    def _save_base_env(self):689        # jitter reset 的基础场景快照,包括危险点、任务和智能体初始状态。690        self._base_danger_points = copy.deepcopy(self.danger_points)691        self._base_tasks = copy.deepcopy(self.tasks)692        self._base_agents = copy.deepcopy(self.agents)693694    def _restore_base_env(self):695        # 从基础场景快照恢复,避免每个 episode 完全重采样。696        self.danger_points = copy.deepcopy(self._base_danger_points)697        self.tasks = copy.deepcopy(self._base_tasks)698        self.agents = copy.deepcopy(self._base_agents)699700    def _jitter_task_positions(self):701        # 只扰动配送任务位置;探索/空闲/补给槽保持固定语义。702        if self.task_position_jitter <= 0.0:703            return704        scale = self.task_position_jitter * self.map_size705        for task in self.tasks:706            if task.type != TaskType.Deliver:707                continue708            base_pos = task.position.copy()709            for _ in range(20):710                offset = self._rand(2) * 2.0 * scale - scale711                new_pos = np.clip(base_pos + offset, 0.0, self.map_size)712                if not any(713                    np.linalg.norm(new_pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance)714                    for dp in self.danger_points715                ):716                    task.position = new_pos717                    break718            else:719                task.position = np.clip(base_pos, 0.0, self.map_size)720721    def _init_map(self):722        # 连续地图:只记录危险点圆心723        self.danger_points = []724        # 危险点数量目前固定,历史代码里保留过按面积缩放的方案。725        num_danger = 10#max(1, int(self.map_size * self.map_size * 0.002))726        for _ in range(num_danger):727            p = (self._rand(2) * self.map_size).astype(float)728            self.danger_points.append({729                'position': p,730                'discovered': False731            })732733    def _init_agents(self):734        # 随机生成智能体能力和位置;位置不能落在危险圆安全范围内。735        self.agents: list[Agent] = []736        for i in range(self.agent_num):737            capbilities = self._rand(self.cap_dim)738            # 采样连续位置,避开危险点中心739            pos = self._rand(2) * self.map_size740            # 保证不在危险半径内741            tries = 0742            while any(np.linalg.norm(pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance) for dp in self.danger_points) and tries < 50:743                pos = self._rand(2) * self.map_size744                tries += 1745746            agent = Agent(i, pos, capbilities, rng=self.rng)747748            self.agents.append(agent)749750    def _init_tasks(self):751        '''随机放置任务,并追加探索、空闲、补给三个类别的辅助任务槽。'''752        self.tasks: list[Task] = []753754        # 先生成所有任务槽,后面会覆盖探索/空闲/补给槽。755        for i in range(self.task_num):756            demand_val = int(self._randint(100, 500))  # 50–100757            pos = self._rand(2) * self.map_size758            tries = 0759            while any(np.linalg.norm(pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance) for dp in self.danger_points) and tries < 200:760                pos = self._rand(2) * self.map_size761                tries += 1762            task = Task(i, np.array(pos, dtype=float), demand_val, self._rand(), self.cap_dim, rng=self.rng)763            if self.use_coalition_delivery and i < self.delivery_task_num:764                deadline_frac = 0.45 + 0.5 * float(self._rand())765                priority = 0.5 + 0.5 * float(self._rand())766                task.urgency = deadline_frac767                task.req_vec = np.array([priority, 0.0, 0.0, 0.0], dtype=float)768            self.tasks.append(task)769770        # 覆盖探索槽。nearest 模式下探索槽位置为 NaN,step 时动态寻找最近未发现任务。771        for sector, explore_id in enumerate(range(self.explore_task_start, self.explore_task_end)):772            if self.explore_action_mode == "survey":773                explore_pos = self._survey_anchor(sector)774            elif self.explore_action_mode == "sector":775                explore_pos = self._explore_sector_anchor(sector)776            else:777                explore_pos = np.array((np.nan, np.nan))778            exploring_task = Task(explore_id, explore_pos, 0, 0.5, self.cap_dim)779            exploring_task.req_vec = np.array([0.25, 0, 0, 0], dtype=float)780            exploring_task.type = TaskType.Explore781            exploring_task.discovered = True782            self.tasks[explore_id] = exploring_task783784        # Idle 槽始终可选,但没有空间目标。785        idle_task = Task(self.__class__.idle_task_id, np.array((np.nan, np.nan)), 0, 0, self.cap_dim)786        idle_task.req_vec = np.array([0, 0, 0, 0])787        idle_task.type = TaskType.Idle788        idle_task.discovered = True789        self.tasks[self.__class__.idle_task_id] = idle_task790791        # Resupply 槽固定在原点,智能体到达后恢复载荷和电量。792        resupply_task = Task(self.__class__.resupply_task_id, np.array((0, 0)), 0, 0, self.cap_dim)793        resupply_task.req_vec = np.array([0, 0, 0, 0])794        resupply_task.type = TaskType.Resupply795        resupply_task.discovered = True796        self.tasks[self.__class__.resupply_task_id] = resupply_task797798        # Delivery-only diagnostics keep the original task/action tensor shapes.799        # Revealing tasks here tests learned allocation and transport without800        # renumbering action slots or replacing the actor checkpoint.801        if self.delivery_tasks_initially_discovered:802            for task in self.tasks:803                if task.type == TaskType.Deliver:804                    task.discovered = True805                    task.discovered_step = 0806807808    def set_reset_mode(self, mode):809        """动态切换 reset 模式,课程学习会调用。"""810        assert mode in ("jitter", "full_random")811        self.env_reset_mode = mode812813    def reset(self, episode=-1):814        self.step_count = 0815        if self.env_reset_mode == "full_random":816            # 每个 episode 完全重采样地图、任务和智能体。817            self._init_map()818            self._init_tasks()819            self._init_agents()820        elif not self._base_initialized:821            # jitter 模式第一次 reset 先生成基础场景并缓存。822            self._init_map()823            self._init_tasks()824            self._init_agents()825            self._save_base_env()826            self._base_initialized = True827        else:828            # jitter 模式后续 episode 恢复基础场景,只扰动配送任务位置。829            self._restore_base_env()830            self._jitter_task_positions()831        self._reset_exploration_coverage()832        # sent_goal 包含所有任务槽的初始需求;辅助任务需求为 0,不影响总配送目标。833        self.sent_goal = np.sum([task.initial_demand for task in self.tasks])834        self.event_new_task_found = 0835        self.event_task_completed = [0 for i in range(self.agent_num)]836        self.event_any_task_completed = 0837        self.global_state = np.zeros((self.agent_num, self.task_num, self.gat_dim))838839        self.total_coalition_changes = 0840        self.total_reconfiguration_decisions = 0841        self._current_decision_mask = np.ones((self.agent_num,), dtype=np.float32)842        self._last_reconfiguration_decision_step = np.zeros(843            (self.agent_num,), dtype=np.int32844        )845        self._last_coalition_switch_step = np.zeros(846            (self.agent_num,), dtype=np.int32847        )848        self.total_alive_agent_steps = 0849        self.response_times = []850851        if episode != -1:852            self.episode = episode853        observations = self._get_observations()854855        active_mask = np.ones((self.agent_num,), dtype=np.float32)856        active_mask = active_mask[..., None]857858        available_actions = self._get_available_actions()859860        return observations, active_mask, available_actions#, info861862    def _get_available_actions(self):863        # 当前策略只能选择“已发现且未完成”的任务槽。864        # Explore/Idle/Resupply 在初始化时设置为 discovered=True,因此一般保持可选。865        task_mask = np.fromiter(866            (1.0 if task.discovered and not task.completed else 0.0 for task in self.tasks),867            dtype=np.float32,868            count=self.task_num,869        )870        available = np.repeat(task_mask[None, :], self.agent_num, axis=0)871        if self.mask_explore_actions:872            available[:, self.explore_task_start : self.explore_task_end] = 0.0873        if self.explore_action_mode == "sector":874            # A relative direction that immediately clips against the map edge875            # is not a meaningful search action. Mask legs shorter than one876            # configured mobility interval separately for every agent.877            for agent_idx, ag in enumerate(self.agents):878                min_leg_distance = self._sector_min_leg_distance(ag)879                for sector, action_id in enumerate(880                    range(self.explore_task_start, self.explore_task_end)881                ):882                    goal = self._explore_sector_nominal_goal(ag, sector)883                    if np.linalg.norm(goal - ag.position) + eps < min_leg_distance:884                        available[agent_idx, action_id] = 0.0885        return available886887    def _reconfiguration_urgency_pressure(self):888        time_ratio = self.step_count / max(1.0, float(self.max_steps))889        pressures = [890            1.0 - min(1.0, max(0.0, float(task.urgency) - time_ratio))891            for task in self.tasks892            if task.type == TaskType.Deliver and task.discovered and not task.completed893        ]894        if not pressures:895            return 0.0896        aggregate = np.sum if self.reconfiguration_urgency_aggregation == "sum" else np.mean897        return float(aggregate(pressures))898899    def _time_limit_reached(self):900        return self.step_count >= self.episode_time_limit901902    def _get_reconfiguration_active_mask(self, key_event, all_still):903        """决定本 step 哪些 agent 会被送入 actor 重新计算动作。904905        这里的 active 只是“有机会/需要被 actor 调用”,不等同于最终一定换联盟。906        learned/hierarchical gate 的职责是在这些机会中继续判断是否保持 current。907        """908909        living = np.array([ag.living for ag in self.agents], dtype=bool)910        completed = np.array(911            [ag.living and ag.current_task_completed for ag in self.agents], dtype=bool912        )913        if all_still:914            # 全体停住时必须让存活 agent 重新进 actor,否则 episode 会卡死。915            return living.astype(np.float32)916        if self.reconfiguration_mode == "legacy":917            return np.array(918                [919                    1.0 if ag.living and (920                        ag.current_task_completed921                        or (ag.idle and key_event)922                        or (923                            self.explore_event_redecision924                            and self.event_new_task_found == 1925                            and self.tasks[ag.coalition].type == TaskType.Explore926                        )927                    ) else 0.0928                    for ag in self.agents929                ],930                dtype=np.float32,931            )932        if self.reconfiguration_mode == "always":933            return living.astype(np.float32)934        interval_due = self.step_count % self.reconfiguration_interval == 0935        if self.reconfiguration_mode == "periodic":936            reconsider_all = interval_due937        elif self.reconfiguration_mode == "event":938            reconsider_all = key_event939        elif self.reconfiguration_mode == "urgency":940            reconsider_all = self._reconfiguration_urgency_pressure() >= self.reconfiguration_urgency_threshold941        else:942            reconsider_all = interval_due or key_event943        return np.where(reconsider_all, living, completed).astype(np.float32)944945    def _allocation_diagnostics(self):946        """Return load-based allocation diagnostics without changing dynamics.947948        This deliberately does not use the historical ``req_vec`` surplus:949        perception, mobility, payload and battery are not additive resources.950        The first diagnostic definition asks only whether the payload already951        committed to a discovered delivery task covers its remaining demand.952        """953        committed_load = np.zeros(self.task_num, dtype=np.float64)954        for ag in self.agents:955            if not ag.living or ag.coalition is None:956                continue957            cid = int(ag.coalition)958            if not (0 <= cid < self.task_num):959                continue960            task = self.tasks[cid]961            if task.type == TaskType.Deliver and task.discovered and not task.completed:962                committed_load[cid] += max(0.0, float(ag.load))963964        task_rows = []965        total_shortage = 0.0966        total_surplus = 0.0967        has_payload_deficit = False968        for task in self.tasks:969            if task.type != TaskType.Deliver or not task.discovered or task.completed:970                continue971            demand = max(0.0, float(task.demand))972            committed = float(committed_load[task.id])973            shortage = max(0.0, demand - committed)974            surplus = max(0.0, committed - demand)975            total_shortage += shortage976            total_surplus += surplus977            has_payload_deficit = has_payload_deficit or shortage > 1e-6978            task_rows.append({979                "task_id": int(task.id),980                "remaining_demand": demand,981                "committed_load": committed,982                "payload_shortage": shortage,983                "payload_surplus": surplus,984            })985986        idle_agents = [987            ag for ag in self.agents988            if ag.living and self.tasks[int(ag.coalition)].type == TaskType.Idle989        ]990        harmful_idle = sum(991            1 for ag in idle_agents992            if float(ag.load) > 1e-6 and has_payload_deficit993        )994        normalizer = max(1.0, float(self.sent_goal))995        return {996            "idle_agents": int(len(idle_agents)),997            "harmful_idle_agents": int(harmful_idle),998            "useful_idle_agents": int(len(idle_agents) - harmful_idle),999            "committed_payload_shortage_ratio": float(total_shortage / normalizer),1000            "committed_payload_surplus_ratio": float(total_surplus / normalizer),1001            "tasks": task_rows,1002        }10031004    def discover_along_segment(self, agent_idx, a, b):1005        # 沿移动线段检查感知范围,标记新发现的配送任务和危险点。1006        # 使用点到线段距离,而不是只看终点距离,避免高速移动穿过目标但漏检。1007        ag = self.agents[agent_idx]1008        per = float(ag.perception)1009        discovered_tasks = []1010        discovered_dp = 01011        self._mark_explored_segment(ag, a, b)10121013        a = np.asarray(a, dtype=float)1014        b = np.asarray(b, dtype=float)1015        ab = b - a1016        denom = float(np.dot(ab, ab))10171018        def point_segment_distances(points):1019            if len(points) == 0:1020                return np.empty((0,), dtype=float)1021            if denom <= eps:1022                return np.linalg.norm(points - a[None, :], axis=1)1023            t = ((points - a[None, :]) @ ab) / denom1024            t = np.clip(t, 0.0, 1.0)1025            proj = a[None, :] + t[:, None] * ab[None, :]1026            return np.linalg.norm(points - proj, axis=1)10271028        task_ids = [1029            idx1030            for idx, task in enumerate(self.tasks)1031            if (not task.discovered)1032            and task.type == TaskType.Deliver1033            and not np.any(np.isnan(task.position))1034        ]1035        if task_ids:1036            task_positions = np.stack([self.tasks[idx].position for idx in task_ids])1037            discovered_mask = point_segment_distances(task_positions) <= per1038            for idx in np.asarray(task_ids, dtype=np.int64)[discovered_mask]:1039                task = self.tasks[int(idx)]1040                task.discovered = True1041                task.discovered_step = self.step_count1042                discovered_tasks.append(int(idx))10431044        danger_ids = [1045            idx for idx, dp in enumerate(self.danger_points) if not dp["discovered"]1046        ]1047        if danger_ids:1048            danger_positions = np.stack(1049                [self.danger_points[idx]["position"] for idx in danger_ids]1050            )1051            discovered_mask = point_segment_distances(danger_positions) <= per1052            for idx in np.asarray(danger_ids, dtype=np.int64)[discovered_mask]:1053                self.danger_points[int(idx)]["discovered"] = True1054                discovered_dp += 11055        return discovered_tasks, discovered_dp10561057    def move_along_path(self, path_pts, start_pos, max_dist):1058        # path_pts 是从当前位置到目标的路点列表;max_dist 是本步最多可移动距离。1059        # 返回新位置和剩余路径。剩余路径首点始终是新位置。1060        pos = np.array(start_pos, dtype=float)1061        rem = []1062        # 保证路径从当前位置开始,避免上一帧路径首点和当前位置存在微小偏差。1063        pts = [tuple(pos)] + [tuple(p) for p in path_pts[1:]] if len(path_pts) > 0 else [tuple(pos)]1064        dist_left = float(max_dist)1065        i = 01066        while dist_left > 1e-8 and i < len(pts)-1:1067            a = np.array(pts[i], dtype=float)1068            b = np.array(pts[i+1], dtype=float)1069            seg_len = np.linalg.norm(b - a)1070            if seg_len <= 1e-8:1071                i += 11072                continue1073            need = min(seg_len - np.linalg.norm(pos - a), dist_left)1074            # 如果位置不在线段开头,计算剩下在线段上还有多长1075            to_end = np.linalg.norm(b - pos)1076            move = min(to_end, dist_left)1077            if move >= to_end - 1e-8:1078                # 到达当前线段终点,继续走下一段。1079                pos = b1080                dist_left -= to_end1081                i += 11082            else:1083                # 本步距离不足以到达线段终点,停在线段中间。1084                direction = (b - pos) / np.linalg.norm(b - pos)1085                pos = pos + direction * move1086                dist_left = 0.01087                break1088        # 剩余路径:从当前位置开始,然后接上未走完的后续路点。1089        remaining = [tuple(pos)]1090        for j in range(i+1, len(pts)):1091            remaining.append(tuple(pts[j]))1092        return pos, remaining109310941095    def step(self, actions):1096        '''1097        参数1098        -------1099    actions : List[int]  长度 = self.agent_num  1100        每个元素为智能体选中的任务 ID  1101        动作语义:0..(task_num-1) 对应任务 ID,task_num 表示 Idle(空闲,不选择任何任务)。1102        为兼容旧版,也接受 -1 表示 Idle。1103        返回1104        -------1105        observations : dict        {'ego':..., 'tasks':..., 'others':...}1106        rewards      : np.ndarray  [N_agents]1107        dones        : np.ndarray  [N_agents]  (全部任务完成或到 max_steps)1108        info         : dict        调试、评估和日志指标1109        '''1110        from datetime import datetime1111        d = d0 = datetime.now().timestamp()1112        self.step_count += 11113        step_reconfiguration_decisions = int(np.count_nonzero(self._current_decision_mask > 0.5))1114        self.total_reconfiguration_decisions += step_reconfiguration_decisions1115        self._last_reconfiguration_decision_step[1116            self._current_decision_mask > 0.51117        ] = self.step_count1118        self.total_alive_agent_steps += int(sum(ag.living for ag in self.agents))11191120        new_discovered = set()1121        explore_legs_planned = 01122        explore_planned_distance = 0.01123        short_explore_legs = 01124        explore_distance_moved = 0.011251126        self.t = np.array([0, 0, 0, 0, 0], float)11271128        # 1. 联盟决策:动作直接写入每个智能体的 coalition。1129        # available_actions 在 wrapper/policy 侧限制可选任务;这里仍容忍越界动作并忽略。1130        for i, raw_a in enumerate(actions):1131            # 兼容 raw_a 是 tensor 或普通 int 的情况1132            try:1133                a = int(raw_a.item())1134            except Exception:1135                a = int(raw_a)11361137            # 当前动作范围是 [0, task_num),其中 Idle 已经是一个普通任务槽。1138            if a < 0 or a >= self.task_num:1139                # 非法动作,忽略1140                continue11411142            self.agents[i].act(a)1143            # 若选择非法任务 ID,忽略,保持原状态11441145        step_coalition_changes = int(sum(ag.coalition_changed for ag in self.agents))1146        self.total_coalition_changes += step_coalition_changes1147        for idx, ag in enumerate(self.agents):1148            if ag.coalition_changed:1149                self._last_coalition_switch_step[idx] = self.step_count11501151        # 2. 首次响应时间:任务被发现后,第一次有活着的智能体加入该任务联盟时记录。1152        for task in self.tasks:1153            if task.type != TaskType.Deliver:1154                continue1155            if not task.discovered or task.completed:1156                continue1157            if task.discovered_step is None:1158                continue1159            if task.first_assigned_step is not None:1160                continue11611162            assigned = any(1163                ag.living and ag.coalition == task.id1164                for ag in self.agents1165            )1166            if assigned:1167                task.first_assigned_step = self.step_count1168                task.response_time = task.first_assigned_step - task.discovered_step1169                self.response_times.append(task.response_time)11701171        # 3. 清空本步事件标志;后续发现、完成、死亡等逻辑会重新写入。1172        self.event_new_task_found      = 01173        self.event_task_completed      = [0 for i in range(self.agent_num)]1174        self.event_any_task_completed  = 011751176        task_caps = np.zeros((self.task_num, self.cap_dim))1177        coalition_sizes = np.zeros(self.task_num, dtype=int)1178        coalitions = [[] for i in range(self.task_num)]11791180        # 4. 依据当前 coalition 聚合每个任务槽上的联盟成员能力和成员列表。1181        # 这份聚合既用于任务完成逻辑,也用于奖励/诊断中的能力匹配度。1182        for i, ag in enumerate(self.agents):1183            c = ag.coalition1184            if c is None or c == -1 or c == self.task_num:1185                continue1186            task_caps[c] += ag.capbilities1187            coalition_sizes[c] += 11188            coalitions[c].append(ag.aid)11891190        # 5. 联盟-任务能力匹配度诊断。1191        # 当前惩罚分摊代码仍保留但未实际写入 rewards,用于后续恢复或分析。11921193        for j, task in enumerate(self.tasks):1194            # 不考虑 Idle 任务和补给任务1195            if j == self.__class__.idle_task_id or j == self.__class__.resupply_task_id:1196                continue1197            # 只对 active 的、未完成的任务算匹配度1198            if coalition_sizes[j] == 0:1199                continue1200            if not task.completed:1201                continue12021203            req = task.req_vec1204            if req is None:1205                continue12061207            cap = task_caps[j]1208            min_val = 0.21209            # 避免除零1210            denom = np.where((cap > -min_val) & (cap < min_val), min_val, cap)12111212            diff = cap - req1213            shortage = np.clip(-diff, 0.0, None)   # cap < req 的部分1214            surplus  = np.clip( diff, 0.0, None)   # cap > req 的部分12151216            # 按平方惩罚,让严重不匹配的惩罚更大1217            penalty_j = (1218                self.w_short  * (float(np.sum(shortage ** 2)) - self.b_short) +1219                self.w_surplus * (float(np.sum(surplus ** 2)) - self.b_surplus)1220            )12211222            if penalty_j <= 0:1223                continue12241225            # 把惩罚均摊给联盟内所有 agent1226            share = penalty_j / float(coalition_sizes[j])12271228            share = np.clip(share, -100, 2)12291230            # for i in coalitions[j]:1231            #     rewards[i] -= share12321233        d = datetime.now().timestamp()1234        self.t[0] = d - d01235        d0 = d1236        1237        # rewards_by_step[1] = rewards - rewards_by_step[0]12381239        # 6. 为没有剩余路径的非空闲智能体生成目标点和路径。1240        # 已有路径的智能体继续执行旧路径,避免每步重规划造成抖动。12411242        danger_positions = [dp['position'] for dp in self.danger_points]12431244        for i, ag in enumerate(self.agents):12451246            # 死亡智能体不规划路径。1247            if not ag.living:1248                continue12491250            # 未走完旧路径时不重新分配路径。1251            if len(ag.path) != 0:1252                continue12531254            task = self.tasks[ag.coalition]12551256            # Idle 槽没有空间目标。1257            if task.type == TaskType.Idle:1258                continue12591260            # 根据任务类型确定目标点:探索任务可动态选择,配送/补给使用任务位置。1261            if task.type == TaskType.Explore:1262                if self.explore_action_mode == "survey":1263                    goal = np.array(task.position, dtype=float)1264                elif self.explore_action_mode == "sector":1265                    goal = self._explore_sector_goal(ag)1266                else:1267                    # nearest 探索:优先朝最近的未发现配送任务移动。1268                    cand_tasks = [task for task in self.tasks if (not task.discovered) and (task.type == TaskType.Deliver)]1269                    if cand_tasks:1270                        # 按欧氏距离排序,选择最近未发现配送任务。1271                        cand_tasks.sort(key=lambda task: np.linalg.norm(task.position - ag.position))1272                        goal = tuple(cand_tasks[0].position)1273                    else:1274                        # 若没有未发现配送任务,则随机探索一个地图点。1275                        goal = tuple((self._rand(2) * self.map_size))12761277                    goal = np.array(goal, dtype=float)12781279            else: # elif coalition.type == TaskType.Deliver or coalition.type == TaskType.Resupply:12801281                goal = task.position12821283            if task.type == TaskType.Explore and self.explore_action_mode == "sector":1284                planned_distance = float(np.linalg.norm(goal - ag.position))1285                explore_legs_planned += 11286                explore_planned_distance += planned_distance1287                if planned_distance + eps < self._sector_min_leg_distance(ag):1288                    short_explore_legs += 112891290            # 根据危险点生成绕行路径。1291            ag.path = fast_path(ag.position, goal, danger_positions, danger_radius=self.danger_radius, clearance=self.clearance)12921293            # 理论上 fast_path 很少返回空;若失败,本步该智能体不移动。1294            if len(ag.path) == 0:1295                print(f'step {self.step_count}, agent {i}: cannot find path for exploration agent')12961297        # 7. 沿路径移动,并在移动线段上触发探索、交付和补给事件。12981299        sent = 01300        completed = 013011302        for i, ag in enumerate(self.agents):13031304            # 死亡智能体不移动。1305            if not ag.living:1306                continue13071308            # 没有路径说明没有空间目标或规划失败。1309            if len(ag.path) == 0:1310                continue13111312            task = self.tasks[ag.coalition]13131314            # Idle 智能体保持当前位置。1315            if task.type == TaskType.Idle:1316                continue13171318            # 只要执行移动就消耗电量。1319            ag.consume()13201321            # mobility 决定本步沿路径最多前进多远。1322            start = ag.position1323            new_pos, remaining = self.move_along_path(ag.path, start, ag.mobility)1324            # 移动过程中可能发现新任务或危险点。1325            discovered_tasks, discovered_dp = self.discover_along_segment(i, start, new_pos)13261327            # 新发现任务会激活探索奖励,并触发 active_mask 让部分智能体重新决策。1328            if len(discovered_tasks) > 0:1329                ag.discovery = len(discovered_tasks) + 0.1 * discovered_dp1330                self.event_new_task_found = 11331                for task_id in discovered_tasks:1332                    new_discovered.add(task_id)13331334            ag.position = np.array(new_pos, dtype=float)1335            if task.type == TaskType.Explore:1336                explore_distance_moved += float(np.linalg.norm(ag.position - start))13371338            # 如果路径已经走到终点,则清空路径。1339            ag.path = remaining if len(remaining) > 1 else []13401341            # 配送/补给任务记录到目标的距离,距离奖励依赖 last_state.dist - dist。1342            if task.type in [TaskType.Deliver, TaskType.Resupply]:1343                dist = np.linalg.norm(ag.position - task.position)1344                ag.dist = float(dist)13451346            if task.type == TaskType.Explore:1347                if self.explore_action_mode == "survey" and len(ag.path) == 0:1348                    task.completed = True1349                    ag.current_task_completed = True1350                elif (1351                    self.explore_action_mode == "sector"1352                    and self.sector_redecision_on_waypoint1353                    and len(ag.path) == 01354                ):1355                    # Sector actions denote one finite search leg, not a permanent1356                    # assignment. Reaching the waypoint should request the next1357                    # policy decision without globally completing the Explore slot.1358                    ag.current_task_completed = True1359                elif len(discovered_tasks) > 0:#ag.unchanged_time >= 10:1360                    ag.current_task_completed = True13611362            # 配送智能体到达任务点后消耗载荷并减少任务 demand。1363            elif task.type == TaskType.Deliver:1364                if not np.any(np.isnan(task.position)) and ag.dist < 1e-3:1365                    ag.complete()1366                    send_amount = ag.deliver(task.demand)1367                    sent += send_amount1368                    task.demand -= send_amount1369                    # 交付量作为联盟贡献同步给同联盟成员,用于联盟运输奖励。1370                    for i in coalitions[task.id]:1371                        self.agents[i].coalition_delivery += ag.delivery13721373                    if task.demand <= 0:1374                        task.completed = True1375                        completed += 11376                        1377                        # 任务完成时,同联盟成员都获得完成事件。1378                        for i in coalitions[task.id]:1379                            self.event_task_completed[i] = 11380                            self.agents[i].coalition_completed = True1381                            self.agents[i].complete()13821383                            self.event_any_task_completed = 113841385            # 补给智能体到达补给点后恢复载荷和电量。1386            elif task.type == TaskType.Resupply:1387                if not np.any(np.isnan(task.position)) and ag.dist < 1e-3:1388                    ag.complete()1389                    ag.resupply()13901391        d = datetime.now().timestamp()1392        self.t[3] = d - d01393        d0 = d13941395        if len(new_discovered) > 0:1396            # 探索发现新任务后,清空探索智能体路径,让它们可以尽快重新决策。1397            for ag in self.agents:1398                if self.tasks[ag.coalition].type == TaskType.Explore:1399                    ag.path = []14001401        # 历史时间惩罚位置保留在这里;当前实际奖励在 _get_rewards 中统一计算。1402        # for i, ag in enumerate(self.agents):1403        #     if ag['living']:1404        #         # 时间惩罚1405        #         rewards[i] -= self.pun_time_ratio * (self.step_count / self.max_steps) # 时间惩罚1406            1407        # 8. 生成下一时刻局部观测。奖励在后面算,但观测已经反映移动/任务状态更新。1408        observations = self._get_observations()14091410        # 9. 终止条件与 active_mask。1411        # done 表示环境回合终止或单个智能体死亡;bad_mask 区分自然终止和时间截断。1412        1413        # key_event 会触发空闲且存活的智能体重新决策。1414        key_event = (self.event_new_task_found == 1 or1415                     self.event_any_task_completed == 1)1416        1417        all_task_completed = all(task.completed for task in self.tasks if task.type == TaskType.Deliver)1418        all_still = all([not ag.living or ag.idle for ag in self.agents])14191420        all_agents_dead = all(not ag.living for ag in self.agents)1421        natural_done = all_task_completed or all_agents_dead1422        time_limit = self._time_limit_reached()14231424        episode_done = natural_done or time_limit1425        dones = np.array([episode_done or not ag.living for ag in self.agents])1426        bad_mask = np.array([0.0 if time_limit and not natural_done else 1.0 for ag in self.agents])1427        bad_mask = bad_mask[..., None]14281429        if episode_done:1430            # episode 结束时所有 agent 都激活,方便 runner flush 最后一帧。1431            active_mask = np.ones((len(self.agents),), dtype=np.float32)1432        else:1433            active_mask = self._get_reconfiguration_active_mask(key_event, all_still)1434        active_mask = active_mask[..., None]1435        self._current_decision_mask = active_mask[:, 0].copy()14361437        available_actions = self._get_available_actions()1438        if self.explore_action_mode == "sector":1439            sector_available = available_actions[1440                :, self.explore_task_start : self.explore_task_end1441            ]1442            unavailable_sector_ratio = float(1443                np.mean(sector_available <= 0.0)1444            )1445        else:1446            unavailable_sector_ratio = 0.014471448        d = datetime.now().timestamp()1449        self.t[4] = d - d01450        d0 = d14511452        # 10. 奖励和任务能力比例诊断。14531454        if self.event_any_task_completed == 1:1455            for ag in self.agents:1456                ag.any_task_completed = True14571458        for i, task in enumerate(self.tasks):1459            # req_ratio 反映当前联盟能力相对需求的富余/缺口,写入任务对象供诊断使用。1460            task.calc_req_ratio(task_caps[i,:])14611462        rewards_ret, rewards_info = self._get_rewards(sent, completed, new_discovered, all_task_completed, all_still)1463        if self.reconfiguration_switch_cost > 0.0:1464            switched = np.array(1465                [1466                    ag.coalition_changed1467                    and not ag.last_state.idle1468                    and not self.tasks[int(ag.last_state.coalition)].completed1469                    for ag in self.agents1470                ],1471                dtype=np.float32,1472            )1473            penalty = self.reconfiguration_switch_cost * switched1474            rewards_ret[:, 0] -= penalty1475            rewards_info[self.reward_keys.index("switch_coalition")] -= penalty14761477        if key_event:1478            pass14791480        # 11. info 主要供日志、评估和离线分析;不进入策略网络。1481        allocation_diagnostics = self._allocation_diagnostics()1482        info = {1483            'agents': [{1484                'position': ag.position,1485                'coalition': ag.coalition,1486                'living': ag.living,1487                'capabilities': ag.capbilities,1488                'load': ag.load,1489                'unchanged': ag.unchanged_time,1490                'idle_wait_steps': ag.idle_wait_steps,1491                } for ag in self.agents],1492            'tasks': [{1493                'position': task.position,1494                'req_vec': task.req_vec,1495                'demand': task.demand,1496                'urgency': task.urgency,1497                'initial_demand': task.initial_demand,1498                'type': int(task.type),1499                'discovered': task.discovered,1500                'completed': task.completed1501                } for task in self.tasks],1502            'danger_points': self.danger_points,1503            't': self.t,1504            'rewards': rewards_info,1505            'reward_keys': self.reward_keys,1506            'sent': sent,1507            'sent_goal': self.sent_goal,1508            'agent_caps': np.stack([ag.capbilities for ag in self.agents]),1509            'task_reqs': np.stack([task.req_vec for task in self.tasks]),1510            'allocation_diagnostics': allocation_diagnostics,1511            'reconfiguration': {1512                'mode': self.reconfiguration_mode,1513                'decisions': step_reconfiguration_decisions,1514                'coalition_changes': step_coalition_changes,1515                'total_decisions': self.total_reconfiguration_decisions,1516                'total_coalition_changes': self.total_coalition_changes,1517                'total_alive_agent_steps': self.total_alive_agent_steps,1518                'urgency_pressure': self._reconfiguration_urgency_pressure(),1519                'mean_response_time': float(np.mean(self.response_times))1520                if self.response_times else 0.0,1521            },1522            'exploration_diagnostics': {1523                'legs_planned': int(explore_legs_planned),1524                'planned_distance': float(explore_planned_distance),1525                'short_legs': int(short_explore_legs),1526                'distance_moved': float(explore_distance_moved),1527                'unavailable_sector_ratio': unavailable_sector_ratio,1528                'explored_area_ratio': float(np.mean(self._explored_cells))1529                if self.explore_coverage_features1530                else 0.0,1531            },1532            'end': self.step_count + 1 if episode_done else 01533        }15341535        for ag in self.agents:1536            if ag.living and self.tasks[int(ag.coalition)].type == TaskType.Idle:1537                ag.idle_wait_steps += 11538            else:1539                ag.idle_wait_steps = 01540            # 最后更新 last_state,并清空本步事件,准备下一次 step。1541            ag.step()154215431544        return observations, rewards_ret, dones, info, active_mask, bad_mask, available_actions154515461547    def _task_feature_matrix(self, include_flags=False, local_visible=False):1548        """构造任务特征矩阵。15491550        local_visible=True 时,未发现或已完成任务在局部观测中置零;1551        include_flags=True 时,为 critic 额外添加 discovered/completed 标志。1552        """15531554        extra_dim = 2 if include_flags else 01555        features = np.zeros((self.task_num, self.per_task_feat + extra_dim), dtype=np.float32)1556        for idx, task in enumerate(self.tasks):1557            if local_visible and (not task.discovered or task.completed):1558                continue1559            pos = task.position1560            if np.isnan(pos[0]) or np.isnan(pos[1]):1561                # Idle 或 nearest Explore 这类无固定空间目标的任务用 0 位置占位。1562                pos = np.array([0.0, 0.0])1563            if self.use_coalition_delivery:1564                features[idx, 0:2] = pos / self.map_size1565                if self.coalition_task_feature_mode == "full":1566                    features[idx, 2:2 + self.cap_dim] = task.req_vec / 5.01567                    features[idx, 2 + self.cap_dim] = task.demand / 500.01568                    deadline_idx = 3 + self.cap_dim1569                    type_idx = 7 + self.cap_dim1570                else:1571                    features[idx, 2] = task.demand / 500.01572                    features[idx, 4] = (1573                        float(task.req_vec[0]) if task.req_vec is not None else 0.01574                    )1575                    deadline_idx = 31576                    type_idx = 51577                if task.type == TaskType.Deliver:1578                    features[idx, deadline_idx] = max(1579                        0.0,1580                        float(task.urgency)1581                        - self.step_count / max(1.0, self.max_steps),1582                    )1583                else:1584                    features[idx, deadline_idx] = 1.01585                features[idx, type_idx] = float(task.type) / float(max(TaskType))1586            else:1587                # 默认 MACF 任务特征:位置、能力需求、剩余需求、任务类型。1588                # req_vec / 5.0 与 HATT residual 中的 hatt_capability_scale 对应。1589                features[idx, 0:2] = pos / self.map_size1590                features[idx, 2:2 + self.cap_dim] = task.req_vec / 5.01591                features[idx, 2 + self.cap_dim] = task.demand / 500.01592                features[idx, 3 + self.cap_dim] = float(task.type) / float(max(TaskType))1593            if include_flags:1594                # 这两个 flag 只给 critic 全局状态,不给 actor 局部任务观测。1595                features[idx, self.per_task_feat] = 1.0 if task.discovered else 0.01596                features[idx, self.per_task_feat + 1] = 1.0 if task.completed else 0.01597        return features15981599    def _agent_feature_matrix(self):1600        """构造所有智能体的状态矩阵,用于 other 观测和 critic 全局状态。"""16011602        features = np.zeros((self.agent_num, self.other_agent_feat_dim), dtype=np.float32)1603        max_coalition = float(max(1, self.task_num - 1))1604        max_agent_type = float(max(AgentType))1605        for idx, ag in enumerate(self.agents):1606            # 注意:这里保留 agent 自己;_get_observations 会在 others 中把对角线置零。1607            features[idx, 0:2] = ag.position / self.map_size1608            features[idx, 2:2 + self.cap_dim] = ag.capbilities1609            features[idx, 2 + self.cap_dim] = float(ag.load) / float(ag.init_load)1610            features[idx, 3 + self.cap_dim] = ag.battery / 1000.01611            features[idx, 4 + self.cap_dim] = float(ag.coalition) / max_coalition1612            features[idx, 5 + self.cap_dim] = float(ag.type) / max_agent_type1613            features[idx, 6 + self.cap_dim] = 1.0 if ag.living else 0.01614        return features16151616    def _task_observation_matrix(self):1617        """Return observer-specific task features.16181619        Ordinary delivery/control slots remain shared.  Observer-relative1620        geometry makes each sector slot describe the executable goal and leg1621        for the corresponding agent.  Coverage additionally places the1622        candidate's novel searched-area ratio and the remaining global1623        unexplored ratio in type-conditioned scalar fields.1624        """16251626        shared = self._task_feature_matrix(local_visible=True)1627        task_obs = np.repeat(shared[None, :, :], self.agent_num, axis=0)1628        if not (1629            (1630                self.explore_coverage_features1631                or self.explore_observer_relative_geometry1632            )1633            and self.explore_action_mode == "sector"1634            and self.use_coalition_delivery1635        ):1636            return task_obs16371638        remaining_unexplored = (1639            float(1.0 - np.mean(self._explored_cells))1640            if self.explore_coverage_features1641            else 0.01642        )1643        for agent_idx, agent in enumerate(self.agents):1644            for sector, task_id in enumerate(1645                range(self.explore_task_start, self.explore_task_end)1646            ):1647                goal = self._explore_sector_nominal_goal(agent, sector)1648                leg_distance = float(np.linalg.norm(goal - agent.position))1649                task_obs[agent_idx, task_id, 0:2] = goal / self.map_size1650                if self.coalition_task_feature_mode == "full":1651                    geometry_start = 4 + self.cap_dim1652                else:1653                    geometry_start = 21654                task_obs[agent_idx, task_id, geometry_start] = (1655                    self._candidate_novel_coverage(agent, goal)1656                )1657                task_obs[agent_idx, task_id, geometry_start + 1] = (1658                    leg_distance / max(eps, self.explore_step_size)1659                )1660                task_obs[agent_idx, task_id, geometry_start + 2] = (1661                    remaining_unexplored1662                )1663        return task_obs16641665    def _get_observations(self):1666        """返回 actor 使用的局部 Dict 观测。16671668        ego:    每个 agent 自己的状态,shape [A, ego_dim]1669        tasks:  每个 agent 看到的任务槽,shape [A, T, task_dim]1670        others: 每个 agent 看到的其他智能体,shape [A, A, other_dim],自身位置置零1671        """16721673        task_obs = self._task_observation_matrix()16741675        agent_features = self._agent_feature_matrix()1676        other_obs = np.repeat(agent_features[None, :, :], self.agent_num, axis=0)1677        self_indices = np.arange(self.agent_num)1678        # others 的第 idx 个 observer 不应看到“另一个自己”,对角线置零。1679        other_obs[self_indices, self_indices, :] = 0.016801681        discovered_deliver = sum(1682            1 for task in self.tasks1683            if task.discovered and task.type == TaskType.Deliver1684        )1685        discovered_ratio = float(discovered_deliver) / float(self.delivery_task_num)1686        max_coalition = float(max(1, self.task_num - 1))1687        max_task_type = float(max(TaskType))1688        ego_obs = np.zeros((self.agent_num, self.ego_feat_dim), dtype=np.float32)1689        for idx, ag in enumerate(self.agents):1690            coalition = self.tasks[ag.coalition]1691            # ego 中保留当前联盟编号和联盟类型,让 actor 知道当前正在执行的任务语义。1692            ego_obs[idx, 0:2] = ag.position / self.map_size1693            ego_obs[idx, 2:2 + self.cap_dim] = ag.capbilities1694            ego_obs[idx, 2 + self.cap_dim] = float(ag.coalition) / max_coalition1695            ego_obs[idx, 3 + self.cap_dim] = float(coalition.type) / max_task_type1696            ego_obs[idx, 4 + self.cap_dim] = float(ag.load) / float(ag.init_load)1697            ego_obs[idx, 5 + self.cap_dim] = discovered_ratio1698            ego_obs[idx, 6 + self.cap_dim] = ag.battery / 1000.016991700        observations = {1701            "ego": ego_obs,1702            "tasks": task_obs.astype(np.float32, copy=False),1703            "others": other_obs.astype(np.float32, copy=False),1704        }1705        if self.reconfiguration_context_features:1706            observations["reconfiguration"] = self._get_reconfiguration_context()1707        return observations17081709    def _get_reconfiguration_context(self):1710        """Return explicit event/time features used only by the optional gate.17111712        Columns are new-task event, any-task-completed event, periodic trigger,1713        focal current-task-completed, focal current-task-available, normalized1714        time since focal decision/switch, episode progress, and urgency pressure.1715        """17161717        context = np.zeros(1718            (self.agent_num, self.reconfiguration_context_dim), dtype=np.float321719        )1720        context[:, 0] = float(self.event_new_task_found == 1)1721        context[:, 1] = float(self.event_any_task_completed == 1)1722        context[:, 2] = float(1723            self.step_count % self.reconfiguration_interval == 01724        )1725        episode_scale = max(1.0, float(self.max_steps))1726        decision_scale = max(1.0, float(self.reconfiguration_interval))1727        for idx, ag in enumerate(self.agents):1728            current = self.tasks[int(ag.coalition)]1729            context[idx, 3] = float(ag.current_task_completed)1730            context[idx, 4] = float(current.discovered and not current.completed)1731            context[idx, 5] = min(1732                1.0,1733                (self.step_count - self._last_reconfiguration_decision_step[idx])1734                / decision_scale,1735            )1736            context[idx, 6] = min(1737                1.0,1738                (self.step_count - self._last_coalition_switch_step[idx])1739                / episode_scale,1740            )1741        context[:, 7] = min(1.0, self.step_count / episode_scale)1742        context[:, 8] = self._reconfiguration_urgency_pressure()1743        if self.reconfiguration_context_dim >= 11:1744            all_still = all(not ag.living or ag.idle for ag in self.agents)1745            # all_still 是全局“不能再靠旧动作推进”的信号;mandatory 是逐 agent1746            # “当前动作已经无效或完成”的信号。二者给 gate 做强制开门判断。1747            context[:, 9] = float(all_still)1748            for idx, ag in enumerate(self.agents):1749                current = self.tasks[int(ag.coalition)]1750                current_invalid = not current.discovered or current.completed1751                context[idx, 10] = float(1752                    all_still or ag.current_task_completed or current_invalid1753                )1754        return context17551756    def get_global_state(self):1757        """返回 critic 使用的特权全局状态。17581759        actor 只能看到局部可见 tasks;critic 可以看到所有任务及 discovered/completed 标志。1760        """17611762        return {1763            "agents": self._agent_feature_matrix(),1764            "tasks": self._task_feature_matrix(include_flags=True),1765            "global": np.array([self.step_count / self.max_steps], dtype=np.float32),1766        }17671768    def _get_rewards(self, sent, completed, new_discovered, all_task_completed, all_still):1769        """选择当前场景的奖励函数,并返回每个 agent 的总奖励和分项奖励。"""17701771        if self.use_coalition_delivery:1772            if self.coalition_delivery_reward_mode == "shaped":1773                return self._get_coalition_delivery_rewards_shaped(1774                    sent, completed, new_discovered, all_task_completed, all_still1775                )1776            if self.coalition_delivery_reward_mode == "delivcoal_v1":1777                return self._get_coalition_delivery_rewards_v1(1778                    sent, completed, new_discovered, all_task_completed, all_still1779                )1780            if self.coalition_delivery_reward_mode == "delivcoal_v2":1781                return self._get_coalition_delivery_rewards_v2(1782                    sent, completed, new_discovered, all_task_completed, all_still1783                )1784            if self.coalition_delivery_reward_mode == "delivcoal_v3":1785                return self._get_coalition_delivery_rewards_potential(1786                    sent,1787                    completed,1788                    new_discovered,1789                    all_task_completed,1790                    all_still,1791                    scale=2.0,1792                    local=False,1793                )1794            if self.coalition_delivery_reward_mode == "delivcoal_v4":1795                return self._get_coalition_delivery_rewards_potential(1796                    sent,1797                    completed,1798                    new_discovered,1799                    all_task_completed,1800                    all_still,1801                    scale=5.0,1802                    local=False,1803                )1804            if self.coalition_delivery_reward_mode == "delivcoal_v5":1805                return self._get_coalition_delivery_rewards_potential(1806                    sent,1807                    completed,1808                    new_discovered,1809                    all_task_completed,1810                    all_still,1811                    scale=2.0,1812                    local=True,1813                )1814            return self._get_coalition_delivery_rewards(1815                sent, completed, new_discovered, all_task_completed, all_still1816            )18171818        # 默认奖励:保留早期探索/配送/补给/完成/距离塑形逻辑。1819        any_explore = any(self._is_explore_task_id(ag.coalition) and ag.living for ag in self.agents)1820        # explore_decay = max(0.1, 1 - self.step_count / (0.5 * self.max_steps))1821        # 已发现任务比例用于衰减探索奖励,前期鼓励发现,后期避免一直探索。1822        discovered_ratio = float([task.discovered and task.type == TaskType.Deliver for task in self.tasks].count(True)) / self.delivery_task_num1823        ep_check_1 = 2001824        ep_check_2 = 6001825        # if self.episode < ep_check_1:1826        #     ep_decay = 0.01827        # elif self.episode < ep_check_2:1828        #     ep_decay = float(self.episode - ep_check_1) / (ep_check_2 - ep_check_1)1829        # else:1830        #     ep_decay = 1.01831        ep_decay = 1.018321833        step_decay = self.step_count / self.max_steps1834        # rewards_1/rewards_2 是历史上两套奖励方案的混合接口;当前 ep_decay=1,只使用 rewards_2。1835        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]1836        rewards_1 = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]1837        rewards_2 = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]183818391840        for i, ag in enumerate(self.agents):18411842            if not ag.dead and not ag.living:1843                ag.coalition = self.idle_task_id1844                continue18451846            coalition_type = self.tasks[ag.coalition].type1847            last_coalition_type = self.tasks[ag.last_state.coalition].type1848            # 切换联盟,且上回合不空闲,惩罚1849            if ag.coalition_changed and not ag.last_state.idle:1850                # 切换到探索(第一回合不罚)1851                if self.step_count > 0 and coalition_type == TaskType.Explore:1852                    # 从运输、补给切换到探索,罚1853                    if last_coalition_type in [TaskType.Deliver, TaskType.Resupply]:1854                        rewards_2[i]['switch_coalition'] -= 1.018551856                # 从运输或补给切换1857                if last_coalition_type in [TaskType.Deliver, TaskType.Resupply]:1858                    rewards_2[i]['switch_coalition'] -= 1.518591860            if ag.coalition_changed and ag.last_state.idle:1861                # 从空闲状态出发时,用载荷比例决定补给/执行任务是否合理。1862                load_ratio = ag.load / int(ag.init_load)1863                rewards_2[i]['switch_coalition'] += (-1.0 if coalition_type == TaskType.Resupply else 1.0) \1864                                                * np.sinh((load_ratio - 0.5) * 2) / np.sinh(1.0) * 2.01865                # if coalition_type == TaskType.Resupply:1866                    # if load_ratio < 0.4:1867                    #     rewards_2[i]['switch_coalition'] += ((0.4 - load_ratio) ** 2) * 518681869                # else:1870                #     rewards_2[i]['switch_coalition'] += load_ratio - 0.518711872            # 死亡惩罚1873            if ag.dead:1874                rewards_2[i]['dead'] -= 5.018751876            # 探索奖励1877            # 发现事件奖励按当前发现比例衰减;没有任何探索者时额外惩罚,避免全体过早放弃探索。1878            rewards_2[i]['discover'] = ag.discovery * (0.1/(discovered_ratio + 0.1) if discovered_ratio<0.25 else 0.1/0.35)1879            rewards_2[i]['discover'] -= 0.2 if not any_explore and discovered_ratio <= 0.9 else 0.018801881            # 运输奖励1882            # deliver 是本步真实交付量,直接转成稠密奖励。1883            rewards_2[i]['deliver'] = ag.delivery * 0.118841885            # 补给奖励1886            # 补给越充分奖励越高;无效补给会通过 -1 基线体现代价。1887            rewards_2[i]['resupply'] += ag.resupply_ratio * 2.0 - 1.0 if ag.resupplied else 0.018881889            # 联盟运输奖励1890            # rewards_2[i]['coalition_deliver']  = ag.coalition_delivery * 0.0718911892            # 联盟完成任务奖励1893            # rewards_2[i]['coalition_complete'] += ag.coalition_completed * 4.018941895            base = 20.01896            time_coef = 20.01897            # 单任务完成奖励预留变量;当前没有写入分项,只保留计算位置。1898            completion_reward = (base + time_coef * (1.0 - self.step_count / self.max_steps)) if ag.coalition_completed else 018991900            if all_task_completed:1901                # 全部配送任务完成时给全体终局奖励,越早完成奖励越高。1902                rewards_2[i]['coalition_complete'] += (1 - self.step_count / self.max_steps) * 20 + 2019031904            # 距离奖励1905            if not ag.coalition_changed \1906                    and coalition_type != TaskType.Idle \1907                    and coalition_type != TaskType.Explore:1908                unchanged_decay = (ag.unchanged_time if ag.unchanged_time < 5.0 else 5.0) / 5.01909                rewards_2[i]['dist'] = 0.002 * (ag.last_state.dist - ag.dist)19101911            # 时间惩罚1912            # 每步固定惩罚起到时间成本作用,所有 agent 都会承担。1913            rewards_2[i]['dist'] -= 0.219141915            if all_still:1916                # 全体静止通常表示策略陷入空转,额外惩罚。1917                rewards_2[i]['dist'] -= 1.019181919            # 回合结束平均奖励1920            # end 分项按当前总交付量和完成数给所有 agent 共享进度信号。1921            rewards_2[i]['end'] += sent / self.sent_goal * 101922            rewards_2[i]['end'] += completed / self.delivery_task_num * 501923            rewards_2[i]['end'] /= 2.019241925        for i in range(self.agent_num):1926            for key in self.reward_keys:1927                # 当前 ep_decay=1,因此 rewards_1 不影响实际奖励。1928                rewards[i][key] = (1 - ep_decay) * rewards_1[i][key] + ep_decay * rewards_2[i][key]19291930        rewards_ret = np.zeros(self.agent_num, dtype=float)1931        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)19321933        for i, rewards_ag in enumerate(rewards):1934            for key, rw in rewards_ag.items():1935                # rewards_ret 给 PPO 使用;rewards_info 保留每个分项,供日志和评估分析。1936                rewards_ret[i] += rw1937                rewards_info[self.reward_keys.index(key)][i] = rw19381939        rewards_ret = rewards_ret[..., None]19401941        # rewards_info = np.concatenate([rewards_info, np.array([ag.coalition for ag in self.agents]).reshape(1, -1)])19421943        return rewards_ret, rewards_info19441945    def _get_coalition_delivery_rewards(self, sent, completed, new_discovered, all_task_completed, all_still):1946        """coalition_delivery 场景的基础奖励版本。19471948        强调配送进度、按时完成、缺货补给和未完成任务的终局惩罚。1949        """19501951        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]1952        discovered_ratio = float(1953            [task.discovered and task.type == TaskType.Deliver for task in self.tasks].count(True)1954        ) / self.delivery_task_num1955        any_explore = any(self._is_explore_task_id(ag.coalition) and ag.living for ag in self.agents)1956        remaining_demand = sum(1957            max(0.0, float(task.demand))1958            for task in self.tasks1959            if task.type == TaskType.Deliver and not task.completed1960        )1961        unfinished_tasks = sum(1962            11963            for task in self.tasks1964            if task.type == TaskType.Deliver and not task.completed1965        )1966        has_remaining_delivery = remaining_demand > 1e-619671968        for i, ag in enumerate(self.agents):1969            if not ag.dead and not ag.living:1970                ag.coalition = self.idle_task_id1971                continue19721973            task = self.tasks[ag.coalition]1974            coalition_type = task.type1975            last_coalition_type = self.tasks[ag.last_state.coalition].type19761977            if ag.coalition_changed and not ag.last_state.idle:1978                rewards[i]["switch_coalition"] -= 0.051979                if last_coalition_type == TaskType.Deliver and coalition_type != TaskType.Deliver:1980                    rewards[i]["switch_coalition"] -= 0.0519811982            if ag.dead:1983                rewards[i]["dead"] -= 5.019841985            rewards[i]["discover"] += ag.discovery * 1.01986            if not any_explore and discovered_ratio < 0.9:1987                rewards[i]["discover"] -= 0.0519881989            if ag.delivery > 0:1990                rewards[i]["deliver"] += 0.05 * ag.delivery1991            elif ag.delivery < 0:1992                rewards[i]["deliver"] -= 0.519931994            if ag.resupplied:1995                rewards[i]["resupply"] += 0.8 * ag.resupply_ratio19961997            if ag.coalition_completed:1998                completed_task = self.tasks[ag.coalition]1999                priority = float(completed_task.req_vec[0]) if completed_task.req_vec is not None else 1.02000                deadline_step = float(completed_task.urgency) * self.max_steps2001                on_time_bonus = max(0.0, (deadline_step - self.step_count) / max(1.0, self.max_steps))2002                rewards[i]["coalition_complete"] += 10.0 * priority + 10.0 * on_time_bonus20032004            if has_remaining_delivery:2005                rewards[i]["dist"] -= 0.0052006                if coalition_type == TaskType.Idle:2007                    rewards[i]["dist"] -= 0.032008                if coalition_type == TaskType.Explore and discovered_ratio >= 0.99:2009                    rewards[i]["discover"] -= 0.052010                if ag.load <= 0 and coalition_type != TaskType.Resupply:2011                    rewards[i]["dist"] -= 0.042012            if coalition_type != TaskType.Idle:2013                rewards[i]["dist"] -= 0.0152014            if coalition_type in (TaskType.Deliver, TaskType.Resupply):2015                rewards[i]["dist"] += 0.001 * (ag.last_state.dist - ag.dist)2016            if all_still:2017                rewards[i]["dist"] -= 0.220182019        if self._time_limit_reached() or all_task_completed:2020            shortage_penalty = (2021                12.0 * remaining_demand / max(1.0, float(self.sent_goal))2022                + 4.0 * unfinished_tasks / max(1.0, float(self.delivery_task_num))2023            )2024            completion_bonus = 15.0 if all_task_completed else 0.02025            for item in rewards:2026                item["end"] += completion_bonus - shortage_penalty20272028        rewards_ret = np.zeros(self.agent_num, dtype=float)2029        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)2030        for i, rewards_ag in enumerate(rewards):2031            for key, rw in rewards_ag.items():2032                rewards_ret[i] += rw2033                rewards_info[self.reward_keys.index(key)][i] = rw2034        return rewards_ret[..., None], rewards_info20352036    def _get_coalition_delivery_rewards_shaped(self, sent, completed, new_discovered, all_task_completed, all_still):2037        """coalition_delivery 场景的 shaped 奖励版本。20382039        在基础版本上加强全局配送进度、剩余 backlog 和 deadline pressure 的连续塑形。2040        """20412042        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]2043        delivery_tasks = [task for task in self.tasks if task.type == TaskType.Deliver]2044        discovered_ratio = float(2045            [task.discovered for task in delivery_tasks].count(True)2046        ) / max(1, self.delivery_task_num)2047        any_explore = any(self._is_explore_task_id(ag.coalition) and ag.living for ag in self.agents)2048        remaining_demand = sum(2049            max(0.0, float(task.demand))2050            for task in delivery_tasks2051            if not task.completed2052        )2053        unfinished_tasks = sum(1 for task in delivery_tasks if not task.completed)2054        has_remaining_delivery = remaining_demand > 1e-62055        time_ratio = self.step_count / max(1.0, float(self.max_steps))2056        sent_goal = max(1.0, float(self.sent_goal))20572058        progress_reward = 25.0 * float(sent) / sent_goal2059        backlog_cost = 0.025 * (remaining_demand / sent_goal) * (0.5 + time_ratio)2060        urgency_pressure = 0.02061        for task in delivery_tasks:2062            if task.completed:2063                continue2064            remaining_ratio = max(0.0, float(task.demand)) / max(1.0, float(task.initial_demand))2065            priority = float(task.req_vec[0]) if task.req_vec is not None else 1.02066            deadline_overrun = max(0.0, time_ratio - float(task.urgency))2067            urgency_pressure += priority * remaining_ratio * (0.25 * time_ratio + deadline_overrun)2068        urgency_cost = 0.02 * urgency_pressure / max(1, self.delivery_task_num)20692070        for i, ag in enumerate(self.agents):2071            if not ag.dead and not ag.living:2072                ag.coalition = self.idle_task_id2073                continue20742075            task = self.tasks[ag.coalition]2076            coalition_type = task.type2077            last_coalition_type = self.tasks[ag.last_state.coalition].type20782079            if ag.coalition_changed and not ag.last_state.idle:2080                rewards[i]["switch_coalition"] -= 0.052081                if last_coalition_type == TaskType.Deliver and coalition_type != TaskType.Deliver:2082                    rewards[i]["switch_coalition"] -= 0.0520832084            if ag.dead:2085                rewards[i]["dead"] -= 5.020862087            rewards[i]["discover"] += ag.discovery * 1.02088            if not any_explore and discovered_ratio < 0.9:2089                rewards[i]["discover"] -= 0.052090            if has_remaining_delivery and coalition_type == TaskType.Explore and discovered_ratio >= 0.99:2091                rewards[i]["discover"] -= 0.0820922093            if ag.delivery > 0:2094                rewards[i]["deliver"] += 0.04 * ag.delivery2095            elif ag.delivery < 0:2096                rewards[i]["deliver"] -= 1.020972098            if ag.resupplied:2099                resource_gain = float(ag.resupply_ratio)2100                rewards[i]["resupply"] += 1.0 * resource_gain2101                if resource_gain <= 0.05:2102                    rewards[i]["resupply"] -= 0.5 * (1.0 - resource_gain / 0.05)21032104            if ag.coalition_completed:2105                completed_task = self.tasks[ag.coalition]2106                priority = float(completed_task.req_vec[0]) if completed_task.req_vec is not None else 1.02107                deadline_step = float(completed_task.urgency) * self.max_steps2108                on_time_bonus = max(0.0, (deadline_step - self.step_count) / max(1.0, self.max_steps))2109                rewards[i]["coalition_complete"] += 8.0 * priority + 8.0 * on_time_bonus21102111            if progress_reward != 0.0:2112                rewards[i]["coalition_deliver"] += progress_reward21132114            if has_remaining_delivery:2115                rewards[i]["dist"] -= backlog_cost + urgency_cost2116                if (2117                    self.idle_time_cost_mode == "legacy"2118                    and coalition_type == TaskType.Idle2119                ):2120                    rewards[i]["dist"] -= 0.042121                if ag.load <= 0 and coalition_type != TaskType.Resupply:2122                    rewards[i]["dist"] -= 0.042123                if self.idle_time_cost_mode == "uniform":2124                    rewards[i]["dist"] -= 0.022125            if (2126                self.idle_time_cost_mode == "legacy"2127                and coalition_type != TaskType.Idle2128            ):2129                rewards[i]["dist"] -= 0.022130            if coalition_type in (TaskType.Deliver, TaskType.Resupply):2131                rewards[i]["dist"] += 0.001 * (ag.last_state.dist - ag.dist)2132            if all_still:2133                rewards[i]["dist"] -= 0.221342135        if self._time_limit_reached() or all_task_completed:2136            shortage_penalty = (2137                14.0 * remaining_demand / sent_goal2138                + 4.0 * unfinished_tasks / max(1.0, float(self.delivery_task_num))2139            )2140            completion_bonus = 15.0 if all_task_completed else 0.02141            for item in rewards:2142                item["end"] += completion_bonus - shortage_penalty21432144        rewards_ret = np.zeros(self.agent_num, dtype=float)2145        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)2146        for i, rewards_ag in enumerate(rewards):2147            for key, rw in rewards_ag.items():2148                rewards_ret[i] += rw2149                rewards_info[self.reward_keys.index(key)][i] = rw2150        return rewards_ret[..., None], rewards_info21512152    def _delivery_task_weight(self, task, urgency_gain=0.5, priority_gain=0.5):2153        priority = float(task.req_vec[0]) if task.req_vec is not None else 1.02154        deadline_left = max(2155            0.0,2156            float(task.urgency) - self.step_count / max(1.0, float(self.max_steps)),2157        )2158        urgency = 1.0 - deadline_left2159        return 1.0 + priority_gain * priority + urgency_gain * urgency21602161    def _coalition_sizes_now(self):2162        sizes = np.zeros(self.task_num, dtype=np.int32)2163        for ag in self.agents:2164            if ag.living and ag.coalition is not None and 0 <= ag.coalition < self.task_num:2165                sizes[ag.coalition] += 12166        return sizes21672168    def _get_coalition_delivery_rewards_potential(2169        self,2170        sent,2171        completed,2172        new_discovered,2173        all_task_completed,2174        all_still,2175        scale,2176        local,2177    ):2178        """delivcoal_v3/v4/v5:保留 shaped 奖励,只追加小幅配送进度势函数项。21792180        v1/v2 重写了 credit assignment,会显著改变 MAPPO 的目标;这里刻意不改原奖励,2181        只把本步真实交付量转成额外进度项,用于检验更稠密的配送进度信号是否能帮助 HATT。2182        """21832184        rewards_ret, rewards_info = self._get_coalition_delivery_rewards_shaped(2185            sent, completed, new_discovered, all_task_completed, all_still2186        )2187        bonus = np.zeros(self.agent_num, dtype=float)21882189        if local:2190            # 局部版本:只给当前配送联盟成员,保留 HATT task-slot credit 的诊断价值。2191            for i, ag in enumerate(self.agents):2192                if not ag.living or ag.coalition is None or not (0 <= ag.coalition < self.task_num):2193                    continue2194                task = self.tasks[ag.coalition]2195                if task.type != TaskType.Deliver or ag.coalition_delivery <= 0:2196                    continue2197                progress = float(ag.coalition_delivery) / max(1.0, float(task.initial_demand))2198                task_weight = self._delivery_task_weight(task, urgency_gain=0.25, priority_gain=0.25)2199                bonus[i] += float(scale) * task_weight * progress2200        else:2201            # 全局版本:所有 agent 共享本步新增配送进度,最大限度保持 shaped 的原始团队目标。2202            step_progress = 0.02203            for ag in self.agents:2204                if not ag.living or ag.coalition is None or not (0 <= ag.coalition < self.task_num):2205                    continue2206                task = self.tasks[ag.coalition]2207                if task.type != TaskType.Deliver or ag.delivery <= 0:2208                    continue2209                progress = float(ag.delivery) / max(1.0, float(task.initial_demand))2210                task_weight = self._delivery_task_weight(task, urgency_gain=0.25, priority_gain=0.25)2211                step_progress += task_weight * progress2212            bonus += float(scale) * step_progress22132214        rewards_ret[:, 0] += bonus2215        rewards_info[self.reward_keys.index("coalition_deliver")] += bonus2216        return rewards_ret, rewards_info22172218    def _get_coalition_delivery_rewards_v1(self, sent, completed, new_discovered, all_task_completed, all_still):2219        """delivcoal_v1:把配送进度奖励从全体共享改为当前任务联盟局部 credit。22202221        目标是验证 HATT 的 pair-wise / coalition scorer 是否需要更直接的任务槽奖励信号。2222        v1 不引入 deadline/priority 过程塑形,只修正 credit assignment。2223        """22242225        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]2226        delivery_tasks = [task for task in self.tasks if task.type == TaskType.Deliver]2227        discovered_ratio = float([task.discovered for task in delivery_tasks].count(True)) / max(1, self.delivery_task_num)2228        any_explore = any(self._is_explore_task_id(ag.coalition) and ag.living for ag in self.agents)2229        remaining_demand = sum(max(0.0, float(task.demand)) for task in delivery_tasks if not task.completed)2230        unfinished_tasks = sum(1 for task in delivery_tasks if not task.completed)2231        has_remaining_delivery = remaining_demand > 1e-622322233        for i, ag in enumerate(self.agents):2234            if not ag.dead and not ag.living:2235                ag.coalition = self.idle_task_id2236                continue22372238            task = self.tasks[ag.coalition]2239            coalition_type = task.type2240            last_coalition_type = self.tasks[ag.last_state.coalition].type22412242            if ag.coalition_changed and not ag.last_state.idle:2243                rewards[i]["switch_coalition"] -= 0.042244                if last_coalition_type == TaskType.Deliver and coalition_type != TaskType.Deliver:2245                    rewards[i]["switch_coalition"] -= 0.0422462247            if ag.dead:2248                rewards[i]["dead"] -= 5.022492250            rewards[i]["discover"] += ag.discovery * 0.82251            if not any_explore and discovered_ratio < 0.9 and coalition_type == TaskType.Idle:2252                rewards[i]["discover"] -= 0.0422532254            if ag.delivery > 0:2255                rewards[i]["deliver"] += 0.03 * ag.delivery2256            elif ag.delivery < 0:2257                rewards[i]["deliver"] -= 0.622582259            if ag.resupplied:2260                rewards[i]["resupply"] += 0.8 * ag.resupply_ratio22612262            if coalition_type == TaskType.Deliver and ag.coalition_delivery > 0:2263                progress = float(ag.coalition_delivery) / max(1.0, float(task.initial_demand))2264                rewards[i]["coalition_deliver"] += 25.0 * progress22652266            if ag.coalition_completed:2267                rewards[i]["coalition_complete"] += 6.022682269            if has_remaining_delivery:2270                if coalition_type == TaskType.Idle:2271                    rewards[i]["dist"] -= 0.0252272                if coalition_type == TaskType.Explore and discovered_ratio >= 0.99:2273                    rewards[i]["discover"] -= 0.042274                if ag.load <= 0 and coalition_type != TaskType.Resupply:2275                    rewards[i]["dist"] -= 0.042276            if coalition_type != TaskType.Idle:2277                rewards[i]["dist"] -= 0.012278            if coalition_type in (TaskType.Deliver, TaskType.Resupply):2279                rewards[i]["dist"] += 0.0015 * (ag.last_state.dist - ag.dist)2280            if all_still:2281                rewards[i]["dist"] -= 0.222822283        if self._time_limit_reached() or all_task_completed:2284            shortage_penalty = (2285                10.0 * remaining_demand / max(1.0, float(self.sent_goal))2286                + 3.0 * unfinished_tasks / max(1.0, float(self.delivery_task_num))2287            )2288            completion_bonus = 15.0 if all_task_completed else 0.02289            for item in rewards:2290                item["end"] += completion_bonus - shortage_penalty22912292        rewards_ret = np.zeros(self.agent_num, dtype=float)2293        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)2294        for i, rewards_ag in enumerate(rewards):2295            for key, rw in rewards_ag.items():2296                rewards_ret[i] += rw2297                rewards_info[self.reward_keys.index(key)][i] = rw2298        return rewards_ret[..., None], rewards_info22992300    def _get_coalition_delivery_rewards_v2(self, sent, completed, new_discovered, all_task_completed, all_still):2301        """delivcoal_v2:v1 + priority/deadline 加权的局部进度奖励和轻量联盟规模约束。23022303        该版本仍只改环境 reward,不改变动作集合、观测结构或 HATT 网络。2304        """23052306        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]2307        delivery_tasks = [task for task in self.tasks if task.type == TaskType.Deliver]2308        discovered_ratio = float([task.discovered for task in delivery_tasks].count(True)) / max(1, self.delivery_task_num)2309        any_explore = any(self._is_explore_task_id(ag.coalition) and ag.living for ag in self.agents)2310        remaining_demand = sum(max(0.0, float(task.demand)) for task in delivery_tasks if not task.completed)2311        unfinished_tasks = sum(1 for task in delivery_tasks if not task.completed)2312        has_remaining_delivery = remaining_demand > 1e-62313        coalition_sizes = self._coalition_sizes_now()23142315        for i, ag in enumerate(self.agents):2316            if not ag.dead and not ag.living:2317                ag.coalition = self.idle_task_id2318                continue23192320            task = self.tasks[ag.coalition]2321            coalition_type = task.type2322            last_coalition_type = self.tasks[ag.last_state.coalition].type23232324            if ag.coalition_changed and not ag.last_state.idle:2325                rewards[i]["switch_coalition"] -= 0.042326                if last_coalition_type == TaskType.Deliver and coalition_type != TaskType.Deliver:2327                    rewards[i]["switch_coalition"] -= 0.0423282329            if ag.dead:2330                rewards[i]["dead"] -= 5.023312332            rewards[i]["discover"] += ag.discovery * 0.82333            if not any_explore and discovered_ratio < 0.9 and coalition_type in (TaskType.Idle, TaskType.Resupply):2334                rewards[i]["discover"] -= 0.0352335            if has_remaining_delivery and coalition_type == TaskType.Explore and discovered_ratio >= 0.99:2336                rewards[i]["discover"] -= 0.0523372338            if ag.delivery > 0:2339                rewards[i]["deliver"] += 0.025 * ag.delivery2340            elif ag.delivery < 0:2341                rewards[i]["deliver"] -= 0.823422343            if ag.resupplied:2344                resource_gain = float(ag.resupply_ratio)2345                rewards[i]["resupply"] += 0.9 * resource_gain2346                if resource_gain <= 0.05:2347                    rewards[i]["resupply"] -= 0.25 * (1.0 - resource_gain / 0.05)23482349            if coalition_type == TaskType.Deliver:2350                task_weight = self._delivery_task_weight(task)2351                if ag.coalition_delivery > 0:2352                    progress = float(ag.coalition_delivery) / max(1.0, float(task.initial_demand))2353                    rewards[i]["coalition_deliver"] += 25.0 * task_weight * progress2354                elif not task.completed and coalition_sizes[task.id] > 2:2355                    rewards[i]["coalition_deliver"] -= 0.003 * float(coalition_sizes[task.id] - 2)23562357            if ag.coalition_completed:2358                completed_task = self.tasks[ag.coalition]2359                task_weight = self._delivery_task_weight(completed_task)2360                rewards[i]["coalition_complete"] += 5.0 * task_weight23612362            if has_remaining_delivery:2363                if coalition_type == TaskType.Idle:2364                    rewards[i]["dist"] -= 0.0252365                if ag.load <= 0 and coalition_type != TaskType.Resupply:2366                    rewards[i]["dist"] -= 0.042367            if coalition_type != TaskType.Idle:2368                rewards[i]["dist"] -= 0.0082369            if coalition_type in (TaskType.Deliver, TaskType.Resupply):2370                rewards[i]["dist"] += 0.0015 * (ag.last_state.dist - ag.dist)2371            if all_still:2372                rewards[i]["dist"] -= 0.223732374        if self._time_limit_reached() or all_task_completed:2375            shortage_penalty = (2376                10.0 * remaining_demand / max(1.0, float(self.sent_goal))2377                + 3.0 * unfinished_tasks / max(1.0, float(self.delivery_task_num))2378            )2379            completion_bonus = 15.0 if all_task_completed else 0.02380            for item in rewards:2381                item["end"] += completion_bonus - shortage_penalty23822383        rewards_ret = np.zeros(self.agent_num, dtype=float)2384        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)2385        for i, rewards_ag in enumerate(rewards):2386            for key, rw in rewards_ag.items():2387                rewards_ret[i] += rw2388                rewards_info[self.reward_keys.index(key)][i] = rw2389        return rewards_ret[..., None], rewards_info