返回代码索引

env_core.py

CapCoal 任务生成与执行语义

原文件 · 1422 行 · 60369 字节

envs/capcoal/env_core.py
SHA-256: b124a2d1d4923295f7c5a73b56eeacd0fa9faaa11102345ea1626fcac82c6a72

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    # distance from point p to segment ab24    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    '''37    Continuous-space approximate path planner.3839    Strategy:40    - Try direct straight-line from start to goal.41    - If the line intersects any danger circle (centered at danger_points with42      radius `danger_radius`), add a simple perpendicular-offset waypoint to43      go around the obstacle, then retry. Repeat up to `max_iter` times.4445    Returns a list of 2-tuples (x,y) representing waypoints including start46    and goal. May return [] only if start==goal.47    '''48    if danger_points is None:49        danger_points = []5051    def segment_intersects_circle(a, b, c, r):52        return dist_point_segment(c, a, b) <= r5354    s = tuple(map(float, start))55    g = tuple(map(float, goal))56    if np.allclose(s, g):57        return [s]5859    waypoints = [s, g]60    iter_count = 061    # iterative fixing: check each segment, if intersects an obstacle, insert detour62    while iter_count < max_iter:63        changed = False64        new_waypoints = [waypoints[0]]65        for i in range(len(waypoints)-1):66            a = waypoints[i]67            b = waypoints[i+1]68            # find first obstacle intersecting this segment69            hit = None70            for c in danger_points:71                if segment_intersects_circle(a, b, c, danger_radius + clearance):72                    hit = c73                    break74            if hit is None:75                new_waypoints.append(b)76                continue77            # compute detour point: closest point on segment to obstacle, then shift perpendicular78            a_np = np.array(a); b_np = np.array(b); c_np = np.array(hit)79            ab = b_np - a_np80            if np.allclose(ab, 0):81                # degenerate, just sidestep82                dir_perp = np.array([1.0, 0.0])83            else:84                t = np.dot(c_np - a_np, ab) / np.dot(ab, ab)85                t = np.clip(t, 0.0, 1.0)86                closest = a_np + t * ab87                # perpendicular direction88                dir_perp = np.array([-ab[1], ab[0]])89                if np.allclose(dir_perp, 0):90                    dir_perp = np.array([1.0, 0.0])91                dir_perp = dir_perp / np.linalg.norm(dir_perp)92                # choose sign by testing which side is farther from other obstacles93            # create two candidate detours (left/right)94            det_dist = danger_radius + clearance95            cand1 = tuple((closest + dir_perp * det_dist).tolist()) if 'closest' in locals() else tuple((a_np + dir_perp * det_dist).tolist())96            cand2 = tuple((closest - dir_perp * det_dist).tolist()) if 'closest' in locals() else tuple((a_np - dir_perp * det_dist).tolist())97            # pick candidate that doesn't immediately collide with any obstacle (or the one with larger margin)98            def collision_score(pt):99                ssum = 0.0100                for c2 in danger_points:101                    d = np.linalg.norm(np.array(pt) - np.array(c2)) - danger_radius102                    ssum += max(0.0, (clearance - d))103                return ssum104105            s1 = collision_score(cand1)106            s2 = collision_score(cand2)107            detour = cand1 if s1 <= s2 else cand2108            # insert detour before b109            new_waypoints.append(detour)110            new_waypoints.append(b)111            changed = True112        waypoints = new_waypoints113        iter_count += 1114        if not changed:115            break116117    # finally compress waypoints (remove near-duplicates)118    out = [tuple(map(float, waypoints[0]))]119    for p in waypoints[1:]:120        if np.linalg.norm(np.array(p) - np.array(out[-1])) > eps:121            out.append(tuple(map(float, p)))122    return out123124class AgentLastState:125    __slots__ = ['coalition', 'new_task_found', 'current_task_completed', 'any_task_completed', 'coalition_completed', 'dist', 'load', 'idle']126    127    def __init__(self):128        self.coalition = EnvCore.idle_task_id129        self.new_task_found = False130        self.current_task_completed = False131        self.any_task_completed = False132        self.coalition_completed = False133        self.dist = 0.0134        self.load = 0.0135        self.idle = True136137class Agent:138    __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        if cid != self.coalition:179            self.coalition = cid180            self.coalition_changed = True181            self.path = []182            self.unchanged_time = 0.0183        else:184            self.unchanged_time += 1.0185186        if cid == EnvCore.idle_task_id:187            self.idle = True188        else:189            self.idle_wait_steps = 0190191    def consume(self):192        if self.living:193            self.battery -= 1194            if self.battery == 0:195                self.living = False196                self.dead = True197198    def deliver(self, demand):199        delivery = np.min([self.load, demand])200        self.load -= delivery201        self.delivery = delivery if delivery > 0 else -10202        self.idle = True203204        if self.delivery < 0:205            pass206            207        return delivery208209    def complete(self):210        self.current_task_completed = True211        self.idle = True212213    def resupply(self):214        self.living = True215        self.dead = False216        self.idle = True217        self.battery = int(self.capbilities[3] * 500) + 500218        self.resupply_ratio = (self.init_load - self.load) / self.init_load219        self.resupplied = True220        self.load = self.init_load221222    def step(self):223        for key in self.last_state.__slots__:224            attr = getattr(self, key)225            setattr(self.last_state, key, attr)226227        self.dead = False228        self.new_task_found = False229        self.current_task_completed = False230        self.any_task_completed = False231        self.coalition_completed = False232        self.coalition_changed = False233        self.discovery = 0.0234        self.delivery = 0.0235        self.coalition_delivery = 0.0236        self.resupply_ratio = 0.0237        self.resupplied = False238        self.idle = False239240241class Task:242    __slots__ = [243        'id', 'cap_dim', 'position', 'demand', 'initial_demand', 'urgency',244        'req_vec', 'req_ratio', 'type', 'discovered', 'completed',245        'discovered_step', 'first_assigned_step', 'response_time'246    ]247248    def __init__(self, cid: int, pos: np.ndarray, demand: int, urgency: float, cap_dim: int, rng=None):249        self.id = cid250        self.cap_dim = cap_dim251        self.position = np.array(pos, dtype=float)252        self.demand = demand253        self.initial_demand = demand254        self.urgency = urgency255        self.req_vec = ((rng.rand(cap_dim) if rng is not None else np.random.rand(cap_dim)) * (demand / 100.0)).astype(float)256        self.req_ratio = np.zeros(cap_dim)257        self.type = TaskType.Deliver258        self.discovered = False259        self.completed = False260261        self.discovered_step = None262        self.first_assigned_step = None263        self.response_time = None264265    def calc_req_ratio(self, cap_now):266        if self.discovered and not self.completed:267            req_vec = self.req_vec268            for dim in range(self.cap_dim):269                if np.isnan(req_vec[dim]) or req_vec[dim] <= 0.0:270                    self.req_ratio[dim] = 0.0271                else:272                    self.req_ratio[dim] = (cap_now[dim] - req_vec[dim]) / req_vec[dim]273        else:274            self.req_ratio = np.zeros(self.cap_dim)275276277278class EnvCore:279280    explore_task_id = -1281    idle_task_id = -1282    resupply_task_id = -1283284    def __init__(self, config):285        deliver_target_num = config.target_num286        self.explore_action_mode = getattr(config, "explore_action_mode", "nearest")287        if self.explore_action_mode not in ("nearest", "sector"):288            raise ValueError(f"Unsupported explore_action_mode: {self.explore_action_mode}")289        self.num_explore_actions = (290            int(getattr(config, "num_explore_actions", 1))291            if self.explore_action_mode == "sector"292            else 1293        )294        if self.num_explore_actions <= 0:295            raise ValueError("num_explore_actions must be positive.")296        self._configured_explore_step_size = getattr(config, "explore_step_size", None)297        self.explore_random_jitter = float(getattr(config, "explore_random_jitter", 0.0))298        self.explore_task_start = deliver_target_num299        self.explore_task_end = deliver_target_num + self.num_explore_actions300        self.__class__.explore_task_id = self.explore_task_start301        self.__class__.idle_task_id = self.explore_task_end302        self.__class__.resupply_task_id = self.explore_task_end + 1303        # initialize using constructor parameters (use defaults if not provided)304        self.map_size = float(config.map_size)             # 地图尺寸 / m (continuous)305        self.explore_step_size = (306            float(self._configured_explore_step_size)307            if self._configured_explore_step_size is not None308            else self.map_size * 0.25309        )310        # obstacle/danger geometry params311        self.danger_radius = float(getattr(config, 'danger_radius', 1.0))312        self.clearance = float(getattr(config, 'clearance', 0.5))313        # keep UAV/UGV split for legacy logic (agent_num param is available but code uses uav/ugv)314        self.uav_num = config.num_uav               # 无人机数量315        self.ugv_num = config.num_ugv               # 无人车数量316        self.uav_perception_rg = tuple(config.uav_perception_rg[0:2])317        self.ugv_perception_rg = tuple(config.ugv_perception_rg[0:2])318        self.delivery_task_num = config.target_num319        self.task_num = config.target_num + self.num_explore_actions + 2320        self.gat_dim = config.gat_dim               # GAT 输出维度321        self.max_steps = config.max_steps           # 最大步数322        self.cap_dim = config.cap_dim               # 能力维度323        self.reconfiguration_mode = str(getattr(config, "reconfiguration_mode", "legacy"))324        valid_reconfiguration_modes = {"legacy", "always", "periodic", "event", "urgency", "learned"}325        if self.reconfiguration_mode not in valid_reconfiguration_modes:326            raise ValueError(f"Unsupported reconfiguration_mode: {self.reconfiguration_mode!r}")327        self.reconfiguration_interval = int(getattr(config, "reconfiguration_interval", 10))328        if self.reconfiguration_interval <= 0:329            raise ValueError("reconfiguration_interval must be positive.")330        self.reconfiguration_urgency_threshold = float(331            getattr(config, "reconfiguration_urgency_threshold", 0.7)332        )333        self.reconfiguration_urgency_aggregation = str(334            getattr(config, "reconfiguration_urgency_aggregation", "mean")335        )336        if self.reconfiguration_urgency_aggregation not in ("mean", "sum"):337            raise ValueError("reconfiguration_urgency_aggregation must be 'mean' or 'sum'.")338        self.reconfiguration_switch_cost = float(339            getattr(config, "reconfiguration_switch_cost", 0.0)340        )341        if self.reconfiguration_switch_cost < 0.0:342            raise ValueError("reconfiguration_switch_cost must be non-negative.")343        self.capcoal_req_mode = getattr(config, "capcoal_req_mode", "random_demand")344        req_mode_aliases = {345            "v0": "random_demand",346            "original": "random_demand",347            "rand_demand": "random_demand",348            "v2a": "demand4",349            "demand": "demand4",350            "demand_vector": "demand4",351            "v2b": "demand1",352            "demand_scalar": "demand1",353            "v2c": "random_identity",354            "identity": "random_identity",355        }356        self.capcoal_req_mode = req_mode_aliases.get(self.capcoal_req_mode, self.capcoal_req_mode)357        if self.capcoal_req_mode not in ("random_demand", "demand4", "demand1", "random_identity"):358            raise ValueError(359                "capcoal_req_mode must be 'random_demand', 'demand4', 'demand1', "360                f"or 'random_identity', got {self.capcoal_req_mode!r}"361            )362363        self.reward_keys = ['switch_coalition', 'dead', 'discover', 'deliver', 'resupply', 'coalition_deliver', 'coalition_complete', 'dist', 'end']364365        # total agents (legacy uses uav+ugv)366        self.agent_num = self.uav_num + self.ugv_num367368        self.step_count = 0                 # 当前步数369        # ego obs = position(2), capabilities(cap_dim), coalition_id(1), coalition_type(1),370        # load(1), discovered_ratio(1), battery(1).371        self.ego_feat_dim = self.cap_dim + 7372        # task obs = normalized position(2), req_vec(cap_dim), demand(1), type(1).373        self.per_task_feat = 2 + self.cap_dim + 1 + 1374        # other-agent obs = position(2), capabilities(cap_dim), load(1),375        # battery(1), coalition_id(1), agent_type(1), living(1).376        self.other_agent_feat_dim = 2 + self.cap_dim + 1 + 1 + 1 + 1 + 1377        self.global_agent_feat_dim = self.other_agent_feat_dim378        # task state additionally exposes discovery/completion flags to the critic.379        self.global_task_feat_dim = self.per_task_feat + 2380        self.global_feat_dim = 1381382        # 观测维度:GAT383        # self.obs_dim = self.cap_dim + 1 + self.task_num * self.per_task_feat + self.gat_dim * self.task_num384385        # 观测维度:无GAT386        self.obs_dim = (387            self.ego_feat_dim388            + self.task_num * self.per_task_feat389            + self.agent_num * self.other_agent_feat_dim390        )391392        # 观测维度:最简393        # self.obs_dim = 1 + 3 + 3 # 设置智能体的观测维度394        # self.obs_dim = self.cap_dim + 1 + self.task_num + self.task_num395        # 动作维度:任务个数 + 1(额外的 Idle 动作,索引为 task_num)396        # 为了兼容性,也接受 -1 表示 Idle397        # RL 输出范围建议为 [0, task_num](或使用 -1 作为 Idle),其中 task_num 表示 Idle(空闲,不选择任何任务)398        self.action_dim = self.task_num399400        self.event_new_task_found       = 0401        self.event_task_completed       = [0 for i in range(self.agent_num)]402        self.event_any_task_completed   = 0403404        self.data_dir = 'data'405406        if config.rewards_config_file:407            with open(config.rewards_config_file, 'r', encoding='utf-8') as f:408                rewards_config = yaml.safe_load(f) or {}409        410            for key, value in rewards_config.items():411                setattr(self, key, value)412413        else:414415            self.pun_change_coalition_event = 0.001416            self.pun_change_coalition = 1417            self.w_short = 2.0   # 能力不足权重(重罚)418            self.w_surplus = 0.3 # 能力过多权重(轻罚)419            self.b_short = 8.0   # 能力不足权重(重罚)420            self.b_surplus = 20.0 # 能力过多权重(轻罚)421            self.rwd_ratio_discover_task = 5422            self.rwd_ratio_discover_dp = 1.0423            self.pun_ag_dead = 10424            self.rwd_ratio_send = 1425            self.rwd_coalition_ratio_send = 0.2426            self.rwd_task_completed  = 200427            self.rwd_coalition_task_completed = 50428            self.rwd_dist_ratio = 3.0429            self.pun_time_ratio = 0.1430431        # 初始化全局状态432        self.global_state = np.zeros((self.agent_num, self.task_num, self.gat_dim))433434        self._seed = None435        self.rng = None436        self._base_initialized = False437        self._base_danger_points = None438        self._base_tasks = None439        self._base_agents = None440        self.task_position_jitter = float(getattr(config, 'task_position_jitter', 0.05))441        self.env_reset_mode = getattr(config, 'env_reset_mode', 'jitter')442443    def seed(self, seed):444        self._seed = int(seed)445        self.rng = np.random.RandomState(self._seed)446        self._base_initialized = False447        self._base_danger_points = None448        self._base_tasks = None449        self._base_agents = None450        return [self._seed]451452    def _rand(self, *args):453        if self.rng is not None:454            return self.rng.rand(*args)455        return np.random.rand(*args)456457    def _randint(self, *args, **kwargs):458        if self.rng is not None:459            return self.rng.randint(*args, **kwargs)460        return np.random.randint(*args, **kwargs)461462    def _choice(self, a):463        if self.rng is not None:464            return self.rng.choice(a)465        return np.random.choice(a)466467    def _explore_sector_goal(self, ag):468        sector = int(ag.coalition - self.explore_task_start)469        sector = max(0, min(self.num_explore_actions - 1, sector))470        sector_width = 2.0 * np.pi / float(self.num_explore_actions)471        angle = sector * sector_width472        if self.explore_random_jitter > 0.0:473            jitter = (float(self._rand()) * 2.0 - 1.0) * self.explore_random_jitter * sector_width474            angle += jitter475        direction = np.array([np.cos(angle), np.sin(angle)], dtype=float)476        goal = ag.position + direction * self.explore_step_size477        return np.clip(goal, 0.0, self.map_size)478479    def _save_base_env(self):480        self._base_danger_points = copy.deepcopy(self.danger_points)481        self._base_tasks = copy.deepcopy(self.tasks)482        self._base_agents = copy.deepcopy(self.agents)483484    def _restore_base_env(self):485        self.danger_points = copy.deepcopy(self._base_danger_points)486        self.tasks = copy.deepcopy(self._base_tasks)487        self.agents = copy.deepcopy(self._base_agents)488489    def _jitter_task_positions(self):490        if self.task_position_jitter <= 0.0:491            return492        scale = self.task_position_jitter * self.map_size493        for task in self.tasks:494            if task.type != TaskType.Deliver:495                continue496            base_pos = task.position.copy()497            for _ in range(20):498                offset = self._rand(2) * 2.0 * scale - scale499                new_pos = np.clip(base_pos + offset, 0.0, self.map_size)500                if not any(501                    np.linalg.norm(new_pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance)502                    for dp in self.danger_points503                ):504                    task.position = new_pos505                    break506            else:507                task.position = np.clip(base_pos, 0.0, self.map_size)508509    def _init_map(self):510        # Continuous map: represent only danger points as circle centers.511        # 连续地图:只记录危险点圆心512        self.danger_points = []513        # number of danger points scaled by area514        # 危险点数量按面积比例决定515        num_danger = 10#max(1, int(self.map_size * self.map_size * 0.002))516        for _ in range(num_danger):517            p = (self._rand(2) * self.map_size).astype(float)518            self.danger_points.append({519                'position': p,520                'discovered': False521            })522523    def _init_agents(self):524        self.agents: list[Agent] = []525        for i in range(self.agent_num):526            capbilities = self._rand(self.cap_dim)527            # 采样连续位置,避开危险点中心528            pos = self._rand(2) * self.map_size529            # 保证不在危险半径内530            tries = 0531            while any(np.linalg.norm(pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance) for dp in self.danger_points) and tries < 50:532                pos = self._rand(2) * self.map_size533                tries += 1534535            agent = Agent(i, pos, capbilities, rng=self.rng)536537            self.agents.append(agent)538539    def _make_task_req_vec(self, demand_val):540        """生成配送任务 req,用于区分 demand 尺度和随机 task 指纹的作用。"""541542        if self.capcoal_req_mode == "random_demand":543            return None544        if self.capcoal_req_mode == "demand4":545            return np.ones(self.cap_dim, dtype=float) * (float(demand_val) / 200.0)546        if self.capcoal_req_mode == "demand1":547            req = np.zeros(self.cap_dim, dtype=float)548            req[0] = float(demand_val) / 200.0549            return req550        if self.capcoal_req_mode == "random_identity":551            return (self._rand(self.cap_dim) * 1.5).astype(float)552        raise RuntimeError(f"Unsupported capcoal_req_mode: {self.capcoal_req_mode}")553554    def _init_tasks(self):555        '''在空地 (map == 0) 上随机放置任务,不会卡死。'''556        self.tasks: list[Task] = []557558        # 采样任务为连续点,避开危险点559        for i in range(self.task_num):560            demand_val = int(self._randint(100, 500))  # 50–100561            pos = self._rand(2) * self.map_size562            tries = 0563            while any(np.linalg.norm(pos - np.array(dp['position'])) <= (self.danger_radius + self.clearance) for dp in self.danger_points) and tries < 200:564                pos = self._rand(2) * self.map_size565                tries += 1566            task = Task(i, np.array(pos, dtype=float), demand_val, self._rand(), self.cap_dim, rng=self.rng)567            req_vec = self._make_task_req_vec(demand_val)568            if req_vec is not None:569                task.req_vec = req_vec570            self.tasks.append(task)571572        # 指定探索联盟的初始任务。sector 模式下每个探索动作代表一个方向。573        for explore_id in range(self.explore_task_start, self.explore_task_end):574            exploring_task = Task(explore_id, np.array((np.nan, np.nan)), 0, 0.5, self.cap_dim)575            exploring_task.req_vec = np.array([3, 3, 0, 1])576            exploring_task.type = TaskType.Explore577            exploring_task.discovered = True578            self.tasks[explore_id] = exploring_task579580        idle_task = Task(self.__class__.idle_task_id, np.array((np.nan, np.nan)), 0, 0, self.cap_dim)581        idle_task.req_vec = np.array([0, 0, 0, 0])582        idle_task.type = TaskType.Idle583        idle_task.discovered = True584        self.tasks[self.__class__.idle_task_id] = idle_task585586        resupply_task = Task(self.__class__.resupply_task_id, np.array((0, 0)), 0, 0, self.cap_dim)587        resupply_task.req_vec = np.array([0, 0, 0, 0])588        resupply_task.type = TaskType.Resupply589        resupply_task.discovered = True590        self.tasks[self.__class__.resupply_task_id] = resupply_task591592593    def set_reset_mode(self, mode):594        """Switch reset mode dynamically (used for curriculum learning)."""595        assert mode in ("jitter", "full_random")596        self.env_reset_mode = mode597598    def reset(self, episode=-1):599        self.step_count = 0600        if self.env_reset_mode == "full_random":601            self._init_map()602            self._init_tasks()603            self._init_agents()604        elif not self._base_initialized:605            self._init_map()606            self._init_tasks()607            self._init_agents()608            self._save_base_env()609            self._base_initialized = True610        else:611            self._restore_base_env()612            self._jitter_task_positions()613        self.sent_goal = np.sum([task.initial_demand for task in self.tasks])614        self.event_new_task_found = 0615        self.event_task_completed = [0 for i in range(self.agent_num)]616        self.event_any_task_completed = 0617        self.global_state = np.zeros((self.agent_num, self.task_num, self.gat_dim))618619        self.total_coalition_changes = 0620        self.total_reconfiguration_decisions = 0621        self._current_decision_mask = np.ones((self.agent_num,), dtype=np.float32)622        self.total_alive_agent_steps = 0623        self.response_times = []624625        if episode != -1:626            self.episode = episode627        observations = self._get_observations()628629        active_mask = np.ones((self.agent_num,), dtype=np.float32)630        active_mask = active_mask[..., None]631632        available_actions = self._get_available_actions()633634        return observations, active_mask, available_actions#, info635636    def _get_available_actions(self):637        task_mask = np.fromiter(638            (1.0 if task.discovered and not task.completed else 0.0 for task in self.tasks),639            dtype=np.float32,640            count=self.task_num,641        )642        return np.repeat(task_mask[None, :], self.agent_num, axis=0)643644    def _reconfiguration_urgency_pressure(self):645        time_ratio = self.step_count / max(1.0, float(self.max_steps))646        pressures = [647            1.0 - min(1.0, max(0.0, float(task.urgency) - time_ratio))648            for task in self.tasks649            if task.type == TaskType.Deliver and task.discovered and not task.completed650        ]651        if not pressures:652            return 0.0653        aggregate = np.sum if self.reconfiguration_urgency_aggregation == "sum" else np.mean654        return float(aggregate(pressures))655656    def _get_reconfiguration_active_mask(self, key_event, all_still):657        living = np.array([ag.living for ag in self.agents], dtype=bool)658        completed = np.array(659            [ag.living and ag.current_task_completed for ag in self.agents], dtype=bool660        )661        if all_still:662            return living.astype(np.float32)663        if self.reconfiguration_mode == "legacy":664            return np.array(665                [666                    1.0 if ag.living and (667                        ag.current_task_completed or (ag.idle and key_event)668                    ) else 0.0669                    for ag in self.agents670                ],671                dtype=np.float32,672            )673        if self.reconfiguration_mode == "always":674            return living.astype(np.float32)675        interval_due = self.step_count % self.reconfiguration_interval == 0676        if self.reconfiguration_mode == "periodic":677            reconsider_all = interval_due678        elif self.reconfiguration_mode == "event":679            reconsider_all = key_event680        elif self.reconfiguration_mode == "urgency":681            reconsider_all = self._reconfiguration_urgency_pressure() >= self.reconfiguration_urgency_threshold682        else:683            reconsider_all = interval_due or key_event684        return np.where(reconsider_all, living, completed).astype(np.float32)685686    def _allocation_diagnostics(self):687        """Return load-based allocation diagnostics without changing dynamics."""688        committed_load = np.zeros(self.task_num, dtype=np.float64)689        for ag in self.agents:690            if not ag.living or ag.coalition is None:691                continue692            cid = int(ag.coalition)693            if not (0 <= cid < self.task_num):694                continue695            task = self.tasks[cid]696            if task.type == TaskType.Deliver and task.discovered and not task.completed:697                committed_load[cid] += max(0.0, float(ag.load))698699        task_rows = []700        total_shortage = 0.0701        total_surplus = 0.0702        has_payload_deficit = False703        for task in self.tasks:704            if task.type != TaskType.Deliver or not task.discovered or task.completed:705                continue706            demand = max(0.0, float(task.demand))707            committed = float(committed_load[task.id])708            shortage = max(0.0, demand - committed)709            surplus = max(0.0, committed - demand)710            total_shortage += shortage711            total_surplus += surplus712            has_payload_deficit = has_payload_deficit or shortage > 1e-6713            task_rows.append({714                "task_id": int(task.id),715                "remaining_demand": demand,716                "committed_load": committed,717                "payload_shortage": shortage,718                "payload_surplus": surplus,719            })720721        idle_agents = [722            ag for ag in self.agents723            if ag.living and self.tasks[int(ag.coalition)].type == TaskType.Idle724        ]725        harmful_idle = sum(726            1 for ag in idle_agents727            if float(ag.load) > 1e-6 and has_payload_deficit728        )729        normalizer = max(1.0, float(self.sent_goal))730        return {731            "idle_agents": int(len(idle_agents)),732            "harmful_idle_agents": int(harmful_idle),733            "useful_idle_agents": int(len(idle_agents) - harmful_idle),734            "committed_payload_shortage_ratio": float(total_shortage / normalizer),735            "committed_payload_surplus_ratio": float(total_surplus / normalizer),736            "tasks": task_rows,737        }738739    def discover_along_segment(self, agent_idx, a, b):740        # 标记任务和危险点为已发现(如果在感知距离内)741        ag = self.agents[agent_idx]742        per = float(ag.perception)743        discovered_tasks = []744        discovered_dp = 0745746        a = np.asarray(a, dtype=float)747        b = np.asarray(b, dtype=float)748        ab = b - a749        denom = float(np.dot(ab, ab))750751        def point_segment_distances(points):752            if len(points) == 0:753                return np.empty((0,), dtype=float)754            if denom <= eps:755                return np.linalg.norm(points - a[None, :], axis=1)756            t = ((points - a[None, :]) @ ab) / denom757            t = np.clip(t, 0.0, 1.0)758            proj = a[None, :] + t[:, None] * ab[None, :]759            return np.linalg.norm(points - proj, axis=1)760761        task_ids = [762            idx763            for idx, task in enumerate(self.tasks)764            if (not task.discovered)765            and task.type == TaskType.Deliver766            and not np.any(np.isnan(task.position))767        ]768        if task_ids:769            task_positions = np.stack([self.tasks[idx].position for idx in task_ids])770            discovered_mask = point_segment_distances(task_positions) <= per771            for idx in np.asarray(task_ids, dtype=np.int64)[discovered_mask]:772                task = self.tasks[int(idx)]773                task.discovered = True774                task.discovered_step = self.step_count775                discovered_tasks.append(int(idx))776777        danger_ids = [778            idx for idx, dp in enumerate(self.danger_points) if not dp["discovered"]779        ]780        if danger_ids:781            danger_positions = np.stack(782                [self.danger_points[idx]["position"] for idx in danger_ids]783            )784            discovered_mask = point_segment_distances(danger_positions) <= per785            for idx in np.asarray(danger_ids, dtype=np.int64)[discovered_mask]:786                self.danger_points[int(idx)]["discovered"] = True787                discovered_dp += 1788        return discovered_tasks, discovered_dp789790    def move_along_path(self, path_pts, start_pos, max_dist):791        # path_pts: 从 start 到 goal 的经过点列表792        # start_pos: 当前的位置793        # 返回 new_pos (ndarray) 和 remaining_path (位置列表)794        pos = np.array(start_pos, dtype=float)795        rem = []796        # 保证路径从当前位置开始797        pts = [tuple(pos)] + [tuple(p) for p in path_pts[1:]] if len(path_pts) > 0 else [tuple(pos)]798        dist_left = float(max_dist)799        i = 0800        while dist_left > 1e-8 and i < len(pts)-1:801            a = np.array(pts[i], dtype=float)802            b = np.array(pts[i+1], dtype=float)803            seg_len = np.linalg.norm(b - a)804            if seg_len <= 1e-8:805                i += 1806                continue807            need = min(seg_len - np.linalg.norm(pos - a), dist_left)808            # 如果位置不在线段开头,计算剩下在线段上还有多长809            to_end = np.linalg.norm(b - pos)810            move = min(to_end, dist_left)811            if move >= to_end - 1e-8:812                # 到达 b813                # reach b814                pos = b815                dist_left -= to_end816                i += 1817            else:818                # 按比例往 b 移动819                direction = (b - pos) / np.linalg.norm(b - pos)820                pos = pos + direction * move821                dist_left = 0.0822                break823        # 剩下的路径:从当前位置开始,然后是原路径中 i 之后的点824        remaining = [tuple(pos)]825        for j in range(i+1, len(pts)):826            remaining.append(tuple(pts[j]))827        return pos, remaining828829830    def step(self, actions):831        '''832        参数833        -------834    actions : List[int]  长度 = self.agent_num  835        每个元素为智能体选中的任务 ID  836        动作语义:0..(task_num-1) 对应任务 ID,task_num 表示 Idle(空闲,不选择任何任务)。837        为兼容旧版,也接受 -1 表示 Idle。838        返回839        -------840        observations : np.ndarray  [N_agents, obs_dim]841        rewards      : np.ndarray  [N_agents]842        dones        : np.ndarray  [N_agents]  (全部任务完成或到 max_steps)843        info         : dict        {'agent_cap':..., 'task_reqs':...}844        '''845        from datetime import datetime846        d = d0 = datetime.now().timestamp()847        self.step_count += 1848        step_reconfiguration_decisions = int(np.count_nonzero(self._current_decision_mask > 0.5))849        self.total_reconfiguration_decisions += step_reconfiguration_decisions850        self.total_alive_agent_steps += int(sum(ag.living for ag in self.agents))851852        new_discovered = set()853854        self.t = np.array([0, 0, 0, 0, 0], float)855856        # -------------------------------------------------857        # 1) 联盟决策:只能选 active_task_ids858        # -------------------------------------------------859        for i, raw_a in enumerate(actions):860            # 兼容 raw_a 是 tensor 或普通 int 的情况861            try:862                a = int(raw_a.item())863            except Exception:864                a = int(raw_a)865866            # 约定:动作索引在 [0, task_num],其中 task_num 表示 Idle;也接受 -1 表示 Idle867            if a < 0 or a >= self.task_num:868                # 非法动作,忽略869                continue870871            self.agents[i].act(a)872            # 若选择非法任务 ID,忽略,保持原状态873874        step_coalition_changes = int(sum(ag.coalition_changed for ag in self.agents))875        self.total_coalition_changes += step_coalition_changes876877        # ---- 新增:记录任务首次响应时间 ----878        for task in self.tasks:879            if task.type != TaskType.Deliver:880                continue881            if not task.discovered or task.completed:882                continue883            if task.discovered_step is None:884                continue885            if task.first_assigned_step is not None:886                continue887888            assigned = any(889                ag.living and ag.coalition == task.id890                for ag in self.agents891            )892            if assigned:893                task.first_assigned_step = self.step_count894                task.response_time = task.first_assigned_step - task.discovered_step895                self.response_times.append(task.response_time)896897        # 重置事件898        self.event_new_task_found      = 0899        self.event_task_completed      = [0 for i in range(self.agent_num)]900        self.event_any_task_completed  = 0901902        task_caps = np.zeros((self.task_num, self.cap_dim))903        coalition_sizes = np.zeros(self.task_num, dtype=int)904        coalitions = [[] for i in range(self.task_num)]905906        # 聚合各任务当前联盟的能力 & 统计联盟规模907        for i, ag in enumerate(self.agents):908            c = ag.coalition909            if c is None or c == -1 or c == self.task_num:910                continue911            task_caps[c] += ag.capbilities912            coalition_sizes[c] += 1913            coalitions[c].append(ag.aid)914915        # -------------------------------916        # 联盟-任务 匹配度惩罚(按任务算,再分摊到联盟内agent)917        # -------------------------------918919        for j, task in enumerate(self.tasks):920            # 不考虑 Idle 任务和补给任务921            if j == self.__class__.idle_task_id or j == self.__class__.resupply_task_id:922                continue923            # 只对 active 的、未完成的任务算匹配度924            if coalition_sizes[j] == 0:925                continue926            if not task.completed:927                continue928929            req = task.req_vec930            if req is None:931                continue932933            cap = task_caps[j]934            min_val = 0.2935            # 避免除零936            denom = np.where((cap > -min_val) & (cap < min_val), min_val, cap)937938            diff = cap - req939            shortage = np.clip(-diff, 0.0, None)   # cap < req 的部分940            surplus  = np.clip( diff, 0.0, None)   # cap > req 的部分941942            # 按平方惩罚,让严重不匹配的惩罚更大943            penalty_j = (944                self.w_short  * (float(np.sum(shortage ** 2)) - self.b_short) +945                self.w_surplus * (float(np.sum(surplus ** 2)) - self.b_surplus)946            )947948            if penalty_j <= 0:949                continue950951            # 把惩罚均摊给联盟内所有 agent952            share = penalty_j / float(coalition_sizes[j])953954            share = np.clip(share, -100, 2)955956            # for i in coalitions[j]:957            #     rewards[i] -= share958959        d = datetime.now().timestamp()960        self.t[0] = d - d0961        d0 = d962        963        # rewards_by_step[1] = rewards - rewards_by_step[0]964965        # -------------------------------------------------966        # 2) 为无目标非空闲智能体分配目标967        # -------------------------------------------------968969        danger_positions = [dp['position'] for dp in self.danger_points]970971        for i, ag in enumerate(self.agents):972973            # 尊重死者974            if not ag.living:975                continue976977            # 没走完的不分配978            if len(ag.path) != 0:979                continue980981            task = self.tasks[ag.coalition]982983            # 空闲智能体不分配984            if task.type == TaskType.Idle:985                continue986987            # 计算目标点和开始点988            if task.type == TaskType.Explore:989                if self.explore_action_mode == "sector":990                    goal = self._explore_sector_goal(ag)991                else:992                    # 找到未发现的非探索任务993                    cand_tasks = [task for task in self.tasks if (not task.discovered) and (task.type == TaskType.Deliver)]994                    if cand_tasks:995                        # 欧几里得距离排序996                        cand_tasks.sort(key=lambda task: np.linalg.norm(task.position - ag.position))997                        goal = tuple(cand_tasks[0].position)998                    else:999                        # 随机探索路径点1000                        goal = tuple((self._rand(2) * self.map_size))10011002                    goal = np.array(goal, dtype=float)10031004            else: # elif coalition.type == TaskType.Deliver or coalition.type == TaskType.Resupply:10051006                goal = task.position1007                1008            # 更新路径1009            ag.path = fast_path(ag.position, goal, danger_positions, danger_radius=self.danger_radius, clearance=self.clearance)10101011            # 找不到路径,跳过移动1012            if len(ag.path) == 0:1013                print(f'step {self.step_count}, agent {i}: cannot find path for exploration agent')10141015        # -------------------------------------------------1016        # 3) 沿路径探索、送货、补给1017        # -------------------------------------------------10181019        sent = 01020        completed = 010211022        for i, ag in enumerate(self.agents):10231024            # 尊重死者1025            if not ag.living:1026                continue10271028            # 路径分配失败的不走1029            if len(ag.path) == 0:1030                continue10311032            task = self.tasks[ag.coalition]10331034            # 空闲智能体不动1035            if task.type == TaskType.Idle:1036                continue10371038            # 电量消耗1039            ag.consume()10401041            # 根据 mobility 按当前路径移动1042            start = ag.position1043            new_pos, remaining = self.move_along_path(ag.path, start, ag.mobility)1044            # 沿着线段发现任务和危险点1045            discovered_tasks, discovered_dp = self.discover_along_segment(i, start, new_pos)10461047            # 探索更新1048            if len(discovered_tasks) > 0:1049                ag.discovery = len(discovered_tasks) + 0.1 * discovered_dp1050                self.event_new_task_found = 11051                for task_id in discovered_tasks:1052                    new_discovered.add(task_id)10531054            ag.position = np.array(new_pos, dtype=float)10551056            # 更新剩下的路径1057            ag.path = remaining if len(remaining) > 1 else []10581059            # 如果是运输或补给智能体,需要记录距离1060            if task.type in [TaskType.Deliver, TaskType.Resupply]:1061                dist = np.linalg.norm(ag.position - task.position)1062                ag.dist = float(dist)10631064            if task.type == TaskType.Explore:1065                if len(discovered_tasks) > 0:#ag.unchanged_time >= 10:1066                    ag.current_task_completed = True10671068            # 如果运输智能体到达任务点,执行交付1069            elif task.type == TaskType.Deliver:1070                if not np.any(np.isnan(task.position)) and ag.dist < 1e-3:1071                    ag.complete()1072                    send_amount = ag.deliver(task.demand)1073                    sent += send_amount1074                    task.demand -= send_amount1075                    for i in coalitions[task.id]:1076                        self.agents[i].coalition_delivery += ag.delivery10771078                    if task.demand <= 0:1079                        task.completed = True1080                        completed += 11081                        1082                        for i in coalitions[task.id]:1083                            self.event_task_completed[i] = 11084                            self.agents[i].coalition_completed = True1085                            self.agents[i].complete()10861087                            self.event_any_task_completed = 110881089            # 如果补给智能体到达补给点,执行补给1090            elif task.type == TaskType.Resupply:1091                if not np.any(np.isnan(task.position)) and ag.dist < 1e-3:1092                    ag.complete()1093                    ag.resupply()10941095        d = datetime.now().timestamp()1096        self.t[3] = d - d01097        d0 = d10981099        if len(new_discovered) > 0:1100            for ag in self.agents:1101                if self.tasks[ag.coalition].type == TaskType.Explore:1102                    ag.path = []11031104        # -------------------------------------------------1105        # 4) step 结束奖惩1106        # -------------------------------------------------1107        # for i, ag in enumerate(self.agents):1108        #     if ag['living']:1109        #         # 时间惩罚1110        #         rewards[i] -= self.pun_time_ratio * (self.step_count / self.max_steps) # 时间惩罚1111            1112        # -------------------------------------------------1113        # 3) 生成观测1114        # -------------------------------------------------1115        observations = self._get_observations()11161117        # -------------------------------------------------1118        # 4) 终止条件1119        # -------------------------------------------------1120        1121        key_event = (self.event_new_task_found == 1 or1122                     self.event_any_task_completed == 1)1123        1124        all_task_completed = all(task.completed for task in self.tasks if task.type == TaskType.Deliver)1125        all_still = all([not ag.living or ag.idle for ag in self.agents])11261127        all_agents_dead = all(not ag.living for ag in self.agents)1128        natural_done = all_task_completed or all_agents_dead1129        time_limit = self.step_count >= self.max_steps11301131        episode_done = natural_done or time_limit1132        dones = np.array([episode_done or not ag.living for ag in self.agents])1133        bad_mask = np.array([0.0 if time_limit and not natural_done else 1.0 for ag in self.agents])1134        bad_mask = bad_mask[..., None]11351136        if episode_done:1137            active_mask = np.ones((len(self.agents),), dtype=np.float32)1138        else:1139            active_mask = self._get_reconfiguration_active_mask(key_event, all_still)1140        active_mask = active_mask[..., None]1141        self._current_decision_mask = active_mask[:, 0].copy()11421143        available_actions = self._get_available_actions()11441145        d = datetime.now().timestamp()1146        self.t[4] = d - d01147        d0 = d11481149        # -------------------------------------------------1150        # rewards1151        # -------------------------------------------------11521153        if self.event_any_task_completed == 1:1154            for ag in self.agents:1155                ag.any_task_completed = True11561157        for i, task in enumerate(self.tasks):1158            task.calc_req_ratio(task_caps[i,:])11591160        rewards_ret, rewards_info = self._get_rewards(sent, completed, new_discovered, all_task_completed, all_still)1161        if self.reconfiguration_switch_cost > 0.0:1162            switched = np.array(1163                [1164                    ag.coalition_changed1165                    and not ag.last_state.idle1166                    and not self.tasks[int(ag.last_state.coalition)].completed1167                    for ag in self.agents1168                ],1169                dtype=np.float32,1170            )1171            penalty = self.reconfiguration_switch_cost * switched1172            rewards_ret[:, 0] -= penalty1173            rewards_info[self.reward_keys.index("switch_coalition")] -= penalty11741175        if key_event:1176            pass11771178        # -------------------------------------------------1179        # 5) info1180        # -------------------------------------------------1181        allocation_diagnostics = self._allocation_diagnostics()1182        info = {1183            'agents': [{1184                'position': ag.position,1185                'coalition': ag.coalition,1186                'living': ag.living,1187                'capabilities': ag.capbilities,1188                'load': ag.load,1189                'unchanged': ag.unchanged_time,1190                'idle_wait_steps': ag.idle_wait_steps,1191                } for ag in self.agents],1192            'tasks': [{1193                'position': task.position,1194                'req_vec': task.req_vec,1195                'demand': task.demand,1196                'urgency': task.urgency,1197                'initial_demand': task.initial_demand,1198                'type': int(task.type),1199                'discovered': task.discovered,1200                'completed': task.completed1201                } for task in self.tasks],1202            'danger_points': self.danger_points,1203            't': self.t,1204            'rewards': rewards_info,1205            'reward_keys': self.reward_keys,1206            'sent': sent,1207            'sent_goal': self.sent_goal,1208            'agent_caps': np.stack([ag.capbilities for ag in self.agents]),1209            'task_reqs': np.stack([task.req_vec for task in self.tasks]),1210            'allocation_diagnostics': allocation_diagnostics,1211            'reconfiguration': {1212                'mode': self.reconfiguration_mode,1213                'decisions': step_reconfiguration_decisions,1214                'coalition_changes': step_coalition_changes,1215                'total_decisions': self.total_reconfiguration_decisions,1216                'total_coalition_changes': self.total_coalition_changes,1217                'total_alive_agent_steps': self.total_alive_agent_steps,1218                'urgency_pressure': self._reconfiguration_urgency_pressure(),1219                'mean_response_time': float(np.mean(self.response_times))1220                if self.response_times else 0.0,1221            },1222            'end': self.step_count + 1 if episode_done else 01223        }12241225        for ag in self.agents:1226            if ag.living and self.tasks[int(ag.coalition)].type == TaskType.Idle:1227                ag.idle_wait_steps += 11228            else:1229                ag.idle_wait_steps = 01230            ag.step()123112321233        return observations, rewards_ret, dones, info, active_mask, bad_mask, available_actions123412351236    def _task_feature_matrix(self, include_flags=False, local_visible=False):1237        extra_dim = 2 if include_flags else 01238        features = np.zeros((self.task_num, self.per_task_feat + extra_dim), dtype=np.float32)1239        for idx, task in enumerate(self.tasks):1240            if local_visible and (not task.discovered or task.completed):1241                continue1242            pos = task.position1243            if np.isnan(pos[0]) or np.isnan(pos[1]):1244                pos = np.array([0.0, 0.0])1245            features[idx, 0:2] = pos / self.map_size1246            features[idx, 2:2 + self.cap_dim] = task.req_vec / 5.01247            features[idx, 2 + self.cap_dim] = task.demand / 500.01248            features[idx, 3 + self.cap_dim] = float(task.type) / float(max(TaskType))1249            if include_flags:1250                features[idx, self.per_task_feat] = 1.0 if task.discovered else 0.01251                features[idx, self.per_task_feat + 1] = 1.0 if task.completed else 0.01252        return features12531254    def _agent_feature_matrix(self):1255        features = np.zeros((self.agent_num, self.other_agent_feat_dim), dtype=np.float32)1256        max_coalition = float(max(1, self.task_num - 1))1257        max_agent_type = float(max(AgentType))1258        for idx, ag in enumerate(self.agents):1259            features[idx, 0:2] = ag.position / self.map_size1260            features[idx, 2:2 + self.cap_dim] = ag.capbilities1261            features[idx, 2 + self.cap_dim] = float(ag.load) / float(ag.init_load)1262            features[idx, 3 + self.cap_dim] = ag.battery / 1000.01263            features[idx, 4 + self.cap_dim] = float(ag.coalition) / max_coalition1264            features[idx, 5 + self.cap_dim] = float(ag.type) / max_agent_type1265            features[idx, 6 + self.cap_dim] = 1.0 if ag.living else 0.01266        return features12671268    def _get_observations(self):1269        """Return per-agent local observations grouped as ego/tasks/others."""1270        task_features = self._task_feature_matrix(local_visible=True)1271        task_obs = np.repeat(task_features[None, :, :], self.agent_num, axis=0)12721273        agent_features = self._agent_feature_matrix()1274        other_obs = np.repeat(agent_features[None, :, :], self.agent_num, axis=0)1275        self_indices = np.arange(self.agent_num)1276        other_obs[self_indices, self_indices, :] = 0.012771278        discovered_deliver = sum(1279            1 for task in self.tasks1280            if task.discovered and task.type == TaskType.Deliver1281        )1282        discovered_ratio = float(discovered_deliver) / float(self.delivery_task_num)1283        max_coalition = float(max(1, self.task_num - 1))1284        max_task_type = float(max(TaskType))1285        ego_obs = np.zeros((self.agent_num, self.ego_feat_dim), dtype=np.float32)1286        for idx, ag in enumerate(self.agents):1287            coalition = self.tasks[ag.coalition]1288            ego_obs[idx, 0:2] = ag.position / self.map_size1289            ego_obs[idx, 2:2 + self.cap_dim] = ag.capbilities1290            ego_obs[idx, 2 + self.cap_dim] = float(ag.coalition) / max_coalition1291            ego_obs[idx, 3 + self.cap_dim] = float(coalition.type) / max_task_type1292            ego_obs[idx, 4 + self.cap_dim] = float(ag.load) / float(ag.init_load)1293            ego_obs[idx, 5 + self.cap_dim] = discovered_ratio1294            ego_obs[idx, 6 + self.cap_dim] = ag.battery / 1000.012951296        return {1297            "ego": ego_obs,1298            "tasks": task_obs.astype(np.float32, copy=False),1299            "others": other_obs.astype(np.float32, copy=False),1300        }13011302    def get_global_state(self):1303        """Return the privileged centralized state used only by the critic."""1304        return {1305            "agents": self._agent_feature_matrix(),1306            "tasks": self._task_feature_matrix(include_flags=True),1307            "global": np.array([self.step_count / self.max_steps], dtype=np.float32),1308        }13091310    def _get_rewards(self, sent, completed, new_discovered, all_task_completed, all_still):1311        any_explore = any(ag.coalition == EnvCore.explore_task_id and ag.living for ag in self.agents)1312        # explore_decay = max(0.1, 1 - self.step_count / (0.5 * self.max_steps))1313        discovered_ratio = float([task.discovered and task.type == TaskType.Deliver for task in self.tasks].count(True)) / self.delivery_task_num1314        ep_check_1 = 2001315        ep_check_2 = 6001316        # if self.episode < ep_check_1:1317        #     ep_decay = 0.01318        # elif self.episode < ep_check_2:1319        #     ep_decay = float(self.episode - ep_check_1) / (ep_check_2 - ep_check_1)1320        # else:1321        #     ep_decay = 1.01322        ep_decay = 1.013231324        step_decay = self.step_count / self.max_steps1325        rewards = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]1326        rewards_1 = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]1327        rewards_2 = [{key: 0.0 for key in self.reward_keys} for _ in range(self.agent_num)]132813291330        for i, ag in enumerate(self.agents):13311332            if not ag.dead and not ag.living:1333                ag.coalition = self.idle_task_id1334                continue13351336            coalition_type = self.tasks[ag.coalition].type1337            last_coalition_type = self.tasks[ag.last_state.coalition].type1338            # 切换联盟,且上回合不空闲,惩罚1339            if ag.coalition_changed and not ag.last_state.idle:1340                # 切换到探索(第一回合不罚)1341                if self.step_count > 0 and coalition_type == TaskType.Explore:1342                    # 从运输、补给切换到探索,罚1343                    if last_coalition_type in [TaskType.Deliver, TaskType.Resupply]:1344                        rewards_2[i]['switch_coalition'] -= 1.013451346                # 从运输或补给切换1347                if last_coalition_type in [TaskType.Deliver, TaskType.Resupply]:1348                    rewards_2[i]['switch_coalition'] -= 1.513491350            if ag.coalition_changed and ag.last_state.idle:1351                load_ratio = ag.load / int(ag.init_load)1352                rewards_2[i]['switch_coalition'] += (-1.0 if coalition_type == TaskType.Resupply else 1.0) \1353                                                * np.sinh((load_ratio - 0.5) * 2) / np.sinh(1.0) * 2.01354                # if coalition_type == TaskType.Resupply:1355                    # if load_ratio < 0.4:1356                    #     rewards_2[i]['switch_coalition'] += ((0.4 - load_ratio) ** 2) * 513571358                # else:1359                #     rewards_2[i]['switch_coalition'] += load_ratio - 0.513601361            # 死亡惩罚1362            if ag.dead:1363                rewards_2[i]['dead'] -= 5.01364            1365            # 探索奖励1366            rewards_2[i]['discover'] = ag.discovery * (0.1/(discovered_ratio + 0.1) if discovered_ratio<0.25 else 0.1/0.35)1367            rewards_2[i]['discover'] -= 0.2 if not any_explore and discovered_ratio <= 0.9 else 0.013681369            # 运输奖励1370            rewards_2[i]['deliver'] = ag.delivery * 0.11371            1372            # 补给奖励1373            rewards_2[i]['resupply'] += ag.resupply_ratio * 2.0 - 1.0 if ag.resupplied else 0.013741375            # 联盟运输奖励1376            # rewards_2[i]['coalition_deliver']  = ag.coalition_delivery * 0.0713771378            # 联盟完成任务奖励1379            # rewards_2[i]['coalition_complete'] += ag.coalition_completed * 4.013801381            base = 20.01382            time_coef = 20.01383            completion_reward = (base + time_coef * (1.0 - self.step_count / self.max_steps)) if ag.coalition_completed else 013841385            if all_task_completed:1386                rewards_2[i]['coalition_complete'] += (1 - self.step_count / self.max_steps) * 20 + 2013871388            # 距离奖励1389            if not ag.coalition_changed \1390                    and coalition_type != TaskType.Idle \1391                    and coalition_type != TaskType.Explore:1392                unchanged_decay = (ag.unchanged_time if ag.unchanged_time < 5.0 else 5.0) / 5.01393                rewards_2[i]['dist'] = 0.002 * (ag.last_state.dist - ag.dist)13941395            # 时间惩罚1396            rewards_2[i]['dist'] -= 0.213971398            if all_still:1399                rewards_2[i]['dist'] -= 1.014001401            # 回合结束平均奖励1402            rewards_2[i]['end'] += sent / self.sent_goal * 101403            rewards_2[i]['end'] += completed / self.delivery_task_num * 501404            rewards_2[i]['end'] /= 2.014051406        for i in range(self.agent_num):1407            for key in self.reward_keys:1408                rewards[i][key] = (1 - ep_decay) * rewards_1[i][key] + ep_decay * rewards_2[i][key]14091410        rewards_ret = np.zeros(self.agent_num, dtype=float)1411        rewards_info = np.zeros((len(self.reward_keys), self.agent_num), dtype=float)14121413        for i, rewards_ag in enumerate(rewards):1414            for key, rw in rewards_ag.items():1415                rewards_ret[i] += rw1416                rewards_info[self.reward_keys.index(key)][i] = rw14171418        rewards_ret = rewards_ret[..., None]14191420        # rewards_info = np.concatenate([rewards_info, np.array([ag.coalition for ag in self.agents]).reshape(1, -1)])14211422        return rewards_ret, rewards_info