返回代码索引

gaRMAPPOPolicy.py

图模型路由、actor/critic 构造与动作接口

原文件 · 870 行 · 38302 字节

algorithms/algorithm/gaRMAPPOPolicy.py
SHA-256: a7e11d5a43393b1f28a8c270b61178342e2c0a9e07a25954fa8da19bb89b6e14

1"""2# @Time    : 2025/12/07 10:303# @Author  : wingong4# @Email   : wy1997331@126.com5# @File    : gaRMAPPOPolicy.py6#7# graph_model="legacy_gat" 保留三种旧 GAT Actor 融合模式:8#   "gat"   — Actor 输入 = 仅 GAT 嵌入(信息瓶颈)9#   "cat"   — Actor 输入 = cat(obs, GAT 嵌入)10#   "gated" — Actor 输入 = cat(obs, tanh(α)*GAT 嵌入),α 可学习11# graph_model="hatt" 使用结构化 Dict 观测、关系专属注意力和任务动作解码器。12# Critic 始终使用与 NoGAT 相同的 centralized state。13"""1415import copy1617import torch18from gym import spaces19from algorithms.algorithm.r_actor_critic import R_Actor, R_Critic20from utils.util import update_linear_schedule2122from algorithms.algorithm.GATNetwork import GATNetwork23from algorithms.algorithm.HATTV10Network import HATTActor24from algorithms.algorithm.HATTV20Network import (25    HATTV20Actor,26    initialize_full_actor_from_compact_reference,27)28from algorithms.algorithm.HATTV20InfoNetwork import (29    HATTV20NoIdleActor,30    HATTV20PhysicsActor,31    HATTV20RawActor,32    HATTV20ResourceActor,33    HATTV20SetContextActor,34    HATTV20TaskAttentionActor,35)36from algorithms.algorithm.HATTV20SpecialNetwork import (37    HATTV20PlainNoIdleActor,38    HATTV20SpecialDetachedActor,39    HATTV20SpecialSplitActor,40)41from algorithms.algorithm.HATTV20TypeConditionedNetwork import (42    HATTV20TypeEmbeddingActor,43    HATTV20TypeGateActor,44)45from algorithms.algorithm.HATTV21Network import HATTV2Actor as HATTV21Actor46from algorithms.algorithm.HATTV22Network import HATTV2Actor as HATTV22Actor47from algorithms.algorithm.HATTV23Network import (48    HATTV23AActor,49    HATTV23BActor,50    HATTV23CActor,51)52from algorithms.algorithm.HATTV24Network import HATTV24Actor53from algorithms.algorithm.HATTV25Network import (54    HATTV25AActor,55    HATTV25BActor,56    HATTV25CActor,57    HATTV25FullActor,58)59from algorithms.algorithm.HATTV26Network import (60    HATTV26AActor,61    HATTV26BActor,62    HATTV26CActor,63    HATTV26FullActor,64)65from algorithms.algorithm.HATTV27Network import (66    HATTV27AActor,67    HATTV27BActor,68    HATTV27Actor,69)70from algorithms.algorithm.HATTV30Network import (71    HATTV30AActor,72    HATTV30BActor,73    HATTV30CActor,74)75from algorithms.algorithm.HATTV31Network import HATTV31AActor, HATTV31BActor76from algorithms.algorithm.HATTV32Network import (77    HATTV32AActor,78    HATTV32BActor,79    HATTV32CActor,80    HATTV32DActor,81)82from algorithms.algorithm.HATTV32RNetwork import (83    HATTV32RFullActor,84    HATTV32RNoMemberActor,85    HATTV32RNoTransitionActor,86)87from algorithms.algorithm.HATTV33AANetwork import HATTV33AAActor88from algorithms.algorithm.HATTV34Network import HATTV34Actor89from algorithms.algorithm.HATTGyMRT2ANetwork import HATTGyMRT2AActor90from algorithms.utils.graph_builder import build_graph91from utils.obs_utils import POLICY_OBS_FEATURE_RANKS, flatten_state_dict9293import numpy as np949596def _initialize_with_seed(factory, seed):97    """Build a module from a local RNG stream without shifting rollout RNG.9899    This is opt-in and used by schema attribution experiments.  Separating the100    actor seed from the critic construction makes equal-shaped actors exactly101    paired even when their centralized-state dimensions differ.102    """103104    if seed is None or int(seed) < 0:105        return factory()106    with torch.random.fork_rng(devices=[]):107        torch.manual_seed(int(seed))108        return factory()109import torch.nn as nn110111from torch_geometric.data import Batch112from typing import Tuple113114115class GAMAPPOPolicy:116    """117    MAPPO Policy  class. Wraps actor and critic networks to compute actions and value function predictions.118119    :param args: (argparse.Namespace) arguments containing relevant model and policy information.120    :param obs_space: (gym.Space) observation space.121    :param cent_obs_space: (gym.Space) value function input space (centralized input for MAPPO, decentralized for IPPO).122    :param action_space: (gym.Space) action space.123    :param device: (torch.device) specifies the device to run on (cpu/gpu).124    """125126    MODE_GAT   = "gat"127    MODE_CAT   = "cat"128    MODE_GATED = "gated"129    GRAPH_LEGACY = "legacy_gat"130    GRAPH_HATT = "hatt"131    GRAPH_HATT_V2_0 = "hatt_v2_0"132    GRAPH_HATT_V2_0_RAW = "hatt_v2_0_raw"133    GRAPH_HATT_V2_0_PHYSICS = "hatt_v2_0_physics"134    GRAPH_HATT_V2_0_TASKATTN = "hatt_v2_0_taskattn"135    GRAPH_HATT_V2_0_NOIDLE = "hatt_v2_0_noidle"136    GRAPH_HATT_V2_0_RESOURCE = "hatt_v2_0_resource"137    GRAPH_HATT_V2_0_SETCTX = "hatt_v2_0_setctx"138    GRAPH_HATT_V2_0_PLAIN_NOIDLE = "hatt_v2_0_plain_noidle"139    GRAPH_HATT_V2_0_DETACHED = "hatt_v2_0_detached"140    GRAPH_HATT_V2_0_SPLIT = "hatt_v2_0_split"141    GRAPH_HATT_V2_0_TYPE_EMBED = "hatt_v2_0_type_embed"142    GRAPH_HATT_V2_0_TYPE_GATE = "hatt_v2_0_type_gate"143    GRAPH_HATT_V2_1 = "hatt_v2_1"144    GRAPH_HATT_V2 = "hatt_v2"145    GRAPH_HATT_V2_3A = "hatt_v2_3a"146    GRAPH_HATT_V2_3B = "hatt_v2_3b"147    GRAPH_HATT_V2_3C = "hatt_v2_3c"148    GRAPH_HATT_V2_4 = "hatt_v2_4"149    GRAPH_HATT_V2_5A = "hatt_v2_5a"150    GRAPH_HATT_V2_5B = "hatt_v2_5b"151    GRAPH_HATT_V2_5C = "hatt_v2_5c"152    GRAPH_HATT_V2_5 = "hatt_v2_5"153    GRAPH_HATT_V2_6A = "hatt_v2_6a"154    GRAPH_HATT_V2_6B = "hatt_v2_6b"155    GRAPH_HATT_V2_6C = "hatt_v2_6c"156    GRAPH_HATT_V2_6 = "hatt_v2_6"157    GRAPH_HATT_V2_7A = "hatt_v2_7a"158    GRAPH_HATT_V2_7B = "hatt_v2_7b"159    GRAPH_HATT_V2_7 = "hatt_v2_7"160    GRAPH_HATT_V3_0A = "hatt_v3_0a"161    GRAPH_HATT_V3_0B = "hatt_v3_0b"162    GRAPH_HATT_V3_0C = "hatt_v3_0c"163    GRAPH_HATT_V3_1A = "hatt_v3_1a"164    GRAPH_HATT_V3_1B = "hatt_v3_1b"165    GRAPH_HATT_V3_2A = "hatt_v3_2a"166    GRAPH_HATT_V3_2B = "hatt_v3_2b"167    GRAPH_HATT_V3_2C = "hatt_v3_2c"168    GRAPH_HATT_V3_2D = "hatt_v3_2d"169    GRAPH_HATT_V3_2R = "hatt_v3_2r"170    GRAPH_HATT_V3_2R_NO_TRANSITION = "hatt_v3_2r_no_transition"171    GRAPH_HATT_V3_2R_NO_MEMBER = "hatt_v3_2r_no_member"172    GRAPH_HATT_V3_3AA = "hatt_v3_3aa"173    GRAPH_HATT_V3_4 = "hatt_v3_4"174    GRAPH_HATT_GYMRT2A = "hatt_gymrt2a"175176    @staticmethod177    def _is_hatt_graph_model(graph_model):178        """Return True for all structured Dict-observation HATT variants."""179        return graph_model.startswith("hatt")180181    def __init__(self, args, obs_space, cent_obs_space, act_space, device=torch.device("cpu")):182        self.device = device183        self.lr = args.lr184        self.critic_lr = args.critic_lr185        self.opti_eps = args.opti_eps186        self.weight_decay = args.weight_decay187188        # GAT 模块189        self.obs_space = obs_space190        self.share_obs_space = cent_obs_space191        self.act_space = act_space192193        self.graph_model = getattr(args, "graph_model", self.GRAPH_LEGACY)194        self.reconfiguration_mode = getattr(195            args, "reconfiguration_mode", "legacy"196        )197        # Checkpoint metadata normally prevents silently evaluating a policy198        # under a different coalition decision schedule.  Causal evaluations199        # may opt in to crossing non-learned schedules (for example legacy vs200        # event), whose actor architectures are identical.  Learned mode is201        # never covered because it adds trainable gate parameters.202        self.allow_reconfiguration_mode_mismatch = bool(203            getattr(args, "allow_reconfiguration_mode_mismatch", False)204        )205        self.reconfiguration_context_features = bool(206            getattr(args, "reconfiguration_context_features", False)207        )208        self.reconfiguration_gate_parameterization = getattr(209            args, "reconfiguration_gate_parameterization", "mixture"210        )211        graph_models = (212            self.GRAPH_LEGACY,213            self.GRAPH_HATT,214            self.GRAPH_HATT_V2_0,215            self.GRAPH_HATT_V2_0_RAW,216            self.GRAPH_HATT_V2_0_PHYSICS,217            self.GRAPH_HATT_V2_0_TASKATTN,218            self.GRAPH_HATT_V2_0_NOIDLE,219            self.GRAPH_HATT_V2_0_RESOURCE,220            self.GRAPH_HATT_V2_0_SETCTX,221            self.GRAPH_HATT_V2_0_PLAIN_NOIDLE,222            self.GRAPH_HATT_V2_0_DETACHED,223            self.GRAPH_HATT_V2_0_SPLIT,224            self.GRAPH_HATT_V2_0_TYPE_EMBED,225            self.GRAPH_HATT_V2_0_TYPE_GATE,226            self.GRAPH_HATT_V2_1,227            self.GRAPH_HATT_V2,228            self.GRAPH_HATT_V2_3A,229            self.GRAPH_HATT_V2_3B,230            self.GRAPH_HATT_V2_3C,231            self.GRAPH_HATT_V2_4,232            self.GRAPH_HATT_V2_5A,233            self.GRAPH_HATT_V2_5B,234            self.GRAPH_HATT_V2_5C,235            self.GRAPH_HATT_V2_5,236            self.GRAPH_HATT_V2_6A,237            self.GRAPH_HATT_V2_6B,238            self.GRAPH_HATT_V2_6C,239            self.GRAPH_HATT_V2_6,240            self.GRAPH_HATT_V2_7A,241            self.GRAPH_HATT_V2_7B,242            self.GRAPH_HATT_V2_7,243            self.GRAPH_HATT_V3_0A,244            self.GRAPH_HATT_V3_0B,245            self.GRAPH_HATT_V3_0C,246            self.GRAPH_HATT_V3_1A,247            self.GRAPH_HATT_V3_1B,248            self.GRAPH_HATT_V3_2A,249            self.GRAPH_HATT_V3_2B,250            self.GRAPH_HATT_V3_2C,251            self.GRAPH_HATT_V3_2D,252            self.GRAPH_HATT_V3_2R,253            self.GRAPH_HATT_V3_2R_NO_TRANSITION,254            self.GRAPH_HATT_V3_2R_NO_MEMBER,255            self.GRAPH_HATT_V3_3AA,256            self.GRAPH_HATT_V3_4,257            self.GRAPH_HATT_GYMRT2A,258        )259        if self.graph_model not in graph_models:260            raise ValueError(261                f"Unknown graph_model={self.graph_model!r}; expected one of {graph_models}."262            )263264        # Legacy GAT fusion mode. HATT has its own task-specific action decoder.265        self.mode = getattr(args, "gat_mode", self.MODE_GATED)266        if self.graph_model == self.GRAPH_LEGACY:267            assert self.mode in (self.MODE_GAT, self.MODE_CAT, self.MODE_GATED), \268                f"Unknown gat_mode: {self.mode!r}, expected 'gat', 'cat', or 'gated'"269270        obs_dim = obs_space.shape[0]271        share_obs_dim = cent_obs_space.shape[0]272        self.num_agents = args.num_agents273        self.num_tasks = act_space.n274        self.cap_dim = args.cap_dim275        spatial_hatt = self.graph_model == self.GRAPH_HATT_GYMRT2A276        self.ego_dim = int(args.gymrt2a_ego_dim) if spatial_hatt else self.cap_dim + 7277        self.other_agent_feat_dim = (278            int(args.gymrt2a_other_dim)279            if spatial_hatt280            else 2 + self.cap_dim + 1 + 1 + 1 + 1 + 1281        )282        others_dim = self.num_agents * self.other_agent_feat_dim283        self.per_task_feat = (obs_dim - self.ego_dim - others_dim) // self.num_tasks284        assert self.ego_dim + self.num_tasks * self.per_task_feat + others_dim == obs_dim, \285            f"Cannot infer per_task_feat from obs_dim={obs_dim}, ego_dim={self.ego_dim}, others_dim={others_dim}, num_tasks={self.num_tasks}"286        self.agent_node_dim = obs_dim287        self.graph_node_dim = 2 + self.cap_dim + 1 + 1 + 1288        self.gat_out = args.gat_out_dim          # GAT 输出维度,例如 32289        self.hatt_v20_task_input_mode = str(290            getattr(args, "hatt_v20_task_input_mode", "native")291        )292        self.hatt_v20_init_scheme = str(293            getattr(args, "hatt_v20_init_scheme", "default")294        )295296        critic_input_dim = share_obs_dim297        fake_share_obs_space = spaces.Box(low=-np.inf, high=np.inf, shape=(critic_input_dim,), dtype=np.float32)298        paired_init_seed = int(getattr(args, "hatt_paired_init_seed", -1))299        self.critic = _initialize_with_seed(300            lambda: R_Critic(args, fake_share_obs_space, self.device),301            paired_init_seed,302        )303304        if self._is_hatt_graph_model(self.graph_model):305            actor_class = {306                self.GRAPH_HATT: HATTActor,307                self.GRAPH_HATT_V2_0: HATTV20Actor,308                self.GRAPH_HATT_V2_0_RAW: HATTV20RawActor,309                self.GRAPH_HATT_V2_0_PHYSICS: HATTV20PhysicsActor,310                self.GRAPH_HATT_V2_0_TASKATTN: HATTV20TaskAttentionActor,311                self.GRAPH_HATT_V2_0_NOIDLE: HATTV20NoIdleActor,312                self.GRAPH_HATT_V2_0_RESOURCE: HATTV20ResourceActor,313                self.GRAPH_HATT_V2_0_SETCTX: HATTV20SetContextActor,314                self.GRAPH_HATT_V2_0_PLAIN_NOIDLE: HATTV20PlainNoIdleActor,315                self.GRAPH_HATT_V2_0_DETACHED: HATTV20SpecialDetachedActor,316                self.GRAPH_HATT_V2_0_SPLIT: HATTV20SpecialSplitActor,317                self.GRAPH_HATT_V2_0_TYPE_EMBED: HATTV20TypeEmbeddingActor,318                self.GRAPH_HATT_V2_0_TYPE_GATE: HATTV20TypeGateActor,319                self.GRAPH_HATT_V2_1: HATTV21Actor,320                self.GRAPH_HATT_V2: HATTV22Actor,321                self.GRAPH_HATT_V2_3A: HATTV23AActor,322                self.GRAPH_HATT_V2_3B: HATTV23BActor,323                self.GRAPH_HATT_V2_3C: HATTV23CActor,324                self.GRAPH_HATT_V2_4: HATTV24Actor,325                self.GRAPH_HATT_V2_5A: HATTV25AActor,326                self.GRAPH_HATT_V2_5B: HATTV25BActor,327                self.GRAPH_HATT_V2_5C: HATTV25CActor,328                self.GRAPH_HATT_V2_5: HATTV25FullActor,329                self.GRAPH_HATT_V2_6A: HATTV26AActor,330                self.GRAPH_HATT_V2_6B: HATTV26BActor,331                self.GRAPH_HATT_V2_6C: HATTV26CActor,332                self.GRAPH_HATT_V2_6: HATTV26FullActor,333                self.GRAPH_HATT_V2_7A: HATTV27AActor,334                self.GRAPH_HATT_V2_7B: HATTV27BActor,335                self.GRAPH_HATT_V2_7: HATTV27Actor,336                self.GRAPH_HATT_V3_0A: HATTV30AActor,337                self.GRAPH_HATT_V3_0B: HATTV30BActor,338                self.GRAPH_HATT_V3_0C: HATTV30CActor,339                self.GRAPH_HATT_V3_1A: HATTV31AActor,340                self.GRAPH_HATT_V3_1B: HATTV31BActor,341                self.GRAPH_HATT_V3_2A: HATTV32AActor,342                self.GRAPH_HATT_V3_2B: HATTV32BActor,343                self.GRAPH_HATT_V3_2C: HATTV32CActor,344                self.GRAPH_HATT_V3_2D: HATTV32DActor,345                self.GRAPH_HATT_V3_2R: HATTV32RFullActor,346                self.GRAPH_HATT_V3_2R_NO_TRANSITION: HATTV32RNoTransitionActor,347                self.GRAPH_HATT_V3_2R_NO_MEMBER: HATTV32RNoMemberActor,348                self.GRAPH_HATT_V3_3AA: HATTV33AAActor,349                self.GRAPH_HATT_V3_4: HATTV34Actor,350                self.GRAPH_HATT_GYMRT2A: HATTGyMRT2AActor,351            }[self.graph_model]352            self.actor = _initialize_with_seed(353                lambda: actor_class(354                    args=args,355                    ego_dim=self.ego_dim,356                    other_dim=self.other_agent_feat_dim,357                    task_dim=self.per_task_feat,358                    num_agents=self.num_agents,359                    num_tasks=self.num_tasks,360                    device=self.device,361                ),362                paired_init_seed + 1 if paired_init_seed >= 0 else -1,363            )364            if self.hatt_v20_init_scheme == "compact_function_preserving":365                if self.graph_model != self.GRAPH_HATT_V2_0:366                    raise ValueError(367                        "compact_function_preserving initialization is limited "368                        "to graph_model=hatt_v2_0"369                    )370                if self.hatt_v20_task_input_mode != "restore_req_scale":371                    raise ValueError(372                        "compact_function_preserving initialization requires "373                        "hatt_v20_task_input_mode=restore_req_scale"374                    )375                if paired_init_seed < 0:376                    raise ValueError(377                        "compact_function_preserving initialization requires "378                        "hatt_paired_init_seed"379                    )380                compact_args = copy.copy(args)381                compact_args.hatt_v20_task_input_mode = "native"382                compact_reference = _initialize_with_seed(383                    lambda: HATTV20Actor(384                        args=compact_args,385                        ego_dim=self.ego_dim,386                        other_dim=self.other_agent_feat_dim,387                        task_dim=6,388                        num_agents=self.num_agents,389                        num_tasks=self.num_tasks,390                        device=self.device,391                    ),392                    paired_init_seed + 1,393                )394                initialize_full_actor_from_compact_reference(395                    self.actor, compact_reference, self.cap_dim396                )397            actor_params = list(self.actor.parameters())398        else:399            self.gat_encoder = GATNetwork(400                node_dim=self.graph_node_dim,401                edge_dim=self.graph_node_dim,402                hidden_dim=args.gat_hidden_dim,403                output_dim=self.gat_out404            ).to(self.device)405            if self.mode in (self.MODE_CAT, self.MODE_GATED):406                actor_input_dim = obs_dim + self.gat_out407            else:408                actor_input_dim = self.gat_out409            if self.mode == self.MODE_GATED:410                self.actor_gat_gate = nn.Parameter(torch.zeros((), device=self.device))411            fake_obs_space = spaces.Box(412                low=-np.inf, high=np.inf, shape=(actor_input_dim,), dtype=np.float32413            )414            self.actor = R_Actor(args, fake_obs_space, self.act_space, self.device)415            actor_params = list(self.actor.parameters()) + list(self.gat_encoder.parameters())416            if self.mode == self.MODE_GATED:417                actor_params += [self.actor_gat_gate]418419        self._actor_params = actor_params420        critic_params = list(self.critic.parameters())421422        self.actor_optimizer = torch.optim.Adam(actor_params,423                                                lr=self.lr, eps=self.opti_eps,424                                                weight_decay=self.weight_decay)425        self.critic_optimizer = torch.optim.Adam(critic_params,426                                                 lr=self.critic_lr,427                                                 eps=self.opti_eps,428                                                 weight_decay=self.weight_decay)429430    def lr_decay(self, episode, episodes):431        """432        Decay the actor and critic learning rates.433        :param episode: (int) current training episode.434        :param episodes: (int) total number of training episodes.435        """436        update_linear_schedule(self.actor_optimizer, episode, episodes, self.lr)437        update_linear_schedule(self.critic_optimizer, episode, episodes, self.critic_lr)438439    def _reshape_batch(self, batch: np.ndarray, unit_size):440        """Reshape a batch that may be either [B, unit_size, dim] or [B*unit_size, dim]."""441        if isinstance(batch, torch.Tensor):442            batch = batch.cpu().numpy()443444        if batch.ndim == 3 and batch.shape[1] == unit_size:445            return batch446447        if batch.ndim == 2 and batch.shape[0] % unit_size == 0:448            return batch.reshape(batch.shape[0] // unit_size, unit_size, -1)449450        raise ValueError(451            f"Cannot reshape batch with shape {batch.shape} using unit_size={unit_size}."452        )453454    def _reshape_obs_dict(self, obs, unit_size):455        if not isinstance(obs, dict):456            return self._reshape_batch(obs, unit_size)457458        out = {}459        for key, value in obs.items():460            if isinstance(value, torch.Tensor):461                value = value.detach().cpu().numpy()462            feature_rank = POLICY_OBS_FEATURE_RANKS[key]463            if value.ndim == feature_rank + 2 and value.shape[1] == unit_size:464                out[key] = value465            elif value.shape[0] % unit_size == 0:466                out[key] = value.reshape(value.shape[0] // unit_size, unit_size, *value.shape[1:])467            else:468                raise ValueError(469                    f"Cannot reshape obs[{key!r}] with shape {value.shape} using unit_size={unit_size}."470                )471        return out472473    def _to_tensor_obs(self, obs):474        if not isinstance(obs, dict):475            return torch.tensor(obs, dtype=torch.float32, device=self.device)476        return {477            k: torch.as_tensor(v, dtype=torch.float32, device=self.device)478            for k, v in obs.items()479        }480481    def _flatten_grouped_obs(self, obs):482        out = {}483        for k, v in obs.items():484            feature_rank = POLICY_OBS_FEATURE_RANKS[k]485            if v.ndim == feature_rank + 2 and v.shape[1] == self.num_agents:486                out[k] = v.reshape(v.shape[0] * v.shape[1], *v.shape[2:])487            else:488                out[k] = v489        return out490491    def _flat_to_grouped_obs(self, obs_flat):492        T, n_agents, _ = obs_flat.shape493        task_start = self.ego_dim494        task_end = task_start + self.num_tasks * self.per_task_feat495        return {496            "ego": obs_flat[..., :self.ego_dim],497            "tasks": obs_flat[..., task_start:task_end].view(498                T, n_agents, self.num_tasks, self.per_task_feat499            ),500            "others": obs_flat[..., task_end:].view(501                T, n_agents, n_agents, self.other_agent_feat_dim502            ),503        }504505    def _build_graph_node_features(self, obs):506        ego = obs["ego"]507        task_obs = obs["tasks"]508        others = obs["others"]509        T, n_agents, _ = ego.shape510511        # others[observer, target] stores target-agent state, with the observer's512        # own row zeroed. Aggregate over observers to recover one node per agent.513        valid_other = (others.abs().sum(dim=-1, keepdim=True) > 0).float()514        other_counts = valid_other.sum(dim=1).clamp_min(1.0)515        other_agent_state = (others * valid_other).sum(dim=1) / other_counts516517        agent_nodes = torch.zeros(518            (T, n_agents, self.graph_node_dim), dtype=torch.float32, device=self.device519        )520        resource_start = 2521        resource_end = resource_start + self.cap_dim522        amount_idx = resource_end523        type_idx = amount_idx + 1524        active_idx = type_idx + 1525526        agent_nodes[..., 0:2] = ego[..., 0:2]                         # position527        agent_nodes[..., resource_start:resource_end] = ego[..., 2:2 + self.cap_dim]528        agent_nodes[..., amount_idx] = ego[..., self.cap_dim + 4]     # load ratio529        agent_nodes[..., type_idx] = other_agent_state[..., self.cap_dim + 5]530        agent_nodes[..., active_idx] = other_agent_state[..., self.cap_dim + 6]531532        # Task features are global for the timestep; average the duplicated533        # per-agent observations.534        task_state = task_obs.mean(dim=1)535        task_nodes = torch.zeros(536            (T, self.num_tasks, self.graph_node_dim), dtype=torch.float32, device=self.device537        )538        task_nodes[..., 0:2] = task_state[..., 0:2]                  # position539        task_nodes[..., resource_start:resource_end] = task_state[..., 2:2 + self.cap_dim]540        task_nodes[..., amount_idx] = task_state[..., 2 + self.cap_dim]541        task_nodes[..., type_idx] = task_state[..., 3 + self.cap_dim]542        task_nodes[..., active_idx] = (task_state.abs().sum(dim=-1) > 0).float()543544        return agent_nodes, task_nodes545546    def _gat_augment(self, obs):547        """548        使用 GAT 对每个时间步的 agent/raw task node 特征建图并编码。549550        参数551        ----552        obs : dict[str, Tensor]553            每个时间步、每个智能体的结构化观测。554        返回555        ----556        agent_emb_batch  : Tensor [T, N_agents, gat_out_dim]557            每个时间步、每个智能体对应的 GAT 节点嵌入。558        global_pool_batch: Tensor [T, gat_out_dim]559            每个时间步整张图的全局池化向量(所有节点平均)。560        """561        T, n_agents, _ = obs["ego"].shape562        agent_nodes, task_nodes = self._build_graph_node_features(obs)563564        data_list = []565        for t in range(T):566            g = build_graph(agent_nodes[t], task_nodes[t])567            data_list.append(g)568569        batch = Batch.from_data_list(data_list)570571        node_emb_all = self.gat_encoder(batch.x, batch.edge_index, batch.edge_attr)572573        N = n_agents + self.num_tasks574        node_emb_split = node_emb_all.view(T, N, -1)575576        agent_emb_batch = node_emb_split[:, :n_agents, :]577        global_pool_batch = node_emb_split.mean(dim=1)578579        return agent_emb_batch, global_pool_batch580581    # ---- 输入构造辅助方法 ----582583    def _build_actor_input(self, obs, agent_emb: torch.Tensor):584        """根据模式构造 Actor 的输入张量。"""585        obs_flat_dict = self._flatten_grouped_obs(obs)586        agent_emb_flat = agent_emb.reshape(-1, self.gat_out)  # [T*N, gat_out]587588        if self.mode == self.MODE_GAT:589            return {"gat": agent_emb_flat}590        elif self.mode == self.MODE_CAT:591            obs_aug = dict(obs_flat_dict)592            obs_aug["gat"] = agent_emb_flat593            return obs_aug594        else:  # MODE_GATED595            alpha = torch.tanh(self.actor_gat_gate)596            obs_aug = dict(obs_flat_dict)597            obs_aug["gat"] = alpha * agent_emb_flat598            return obs_aug599600    @staticmethod601    def _build_critic_input(cent_obs):602        cent_obs = flatten_state_dict(cent_obs)603        if getattr(cent_obs, "ndim", 0) > 2:604            cent_obs = cent_obs.reshape(-1, cent_obs.shape[-1])605        return cent_obs606607    def get_gat_gate_values(self) -> Tuple[float, float]:608        """Return the actor gate and a zero compatibility value for the removed critic gate."""609        if self.graph_model != self.GRAPH_LEGACY or self.mode != self.MODE_GATED:610            return 0.0, 0.0611        actor_alpha = torch.tanh(self.actor_gat_gate).detach().cpu().item()612        return float(actor_alpha), 0.0613614    def get_actor_parameters(self):615        return self._actor_params616617    def get_actor_checkpoint(self):618        checkpoint = {619            "graph_model": self.graph_model,620            "reconfiguration_mode": self.reconfiguration_mode,621            "reconfiguration_context_features": self.reconfiguration_context_features,622            "reconfiguration_gate_parameterization": self.reconfiguration_gate_parameterization,623            "hatt_v20_task_input_mode": self.hatt_v20_task_input_mode,624            "hatt_v20_init_scheme": self.hatt_v20_init_scheme,625            "actor": self.actor.state_dict(),626        }627        if self.graph_model == self.GRAPH_LEGACY:628            checkpoint["gat_encoder"] = self.gat_encoder.state_dict()629            if self.mode == self.MODE_GATED:630                checkpoint["actor_gat_gate"] = self.actor_gat_gate.detach().cpu()631        return checkpoint632633    def load_actor_checkpoint(self, checkpoint):634        if "actor" not in checkpoint:635            # Backward compatibility with legacy actor-only checkpoints.636            self.actor.load_state_dict(checkpoint)637            return638        saved_model = checkpoint.get("graph_model", self.GRAPH_LEGACY)639        compatible_v20_checkpoint = (640            (641                self.graph_model == self.GRAPH_HATT_V2_0642                and saved_model == self.GRAPH_HATT_V2643            )644            or (645                self.graph_model == self.GRAPH_HATT_V2_0_NOIDLE646                and saved_model == self.GRAPH_HATT_V2_0_PHYSICS647            )648        )649        if saved_model != self.graph_model and not compatible_v20_checkpoint:650            raise ValueError(651                f"Checkpoint graph_model={saved_model!r} does not match current {self.graph_model!r}."652            )653        saved_reconfiguration_mode = checkpoint.get(654            "reconfiguration_mode", "legacy"655        )656        mode_mismatch = saved_reconfiguration_mode != self.reconfiguration_mode657        safe_nonlearned_intervention = (658            self.allow_reconfiguration_mode_mismatch659            and "learned" not in (660                saved_reconfiguration_mode,661                self.reconfiguration_mode,662            )663        )664        if mode_mismatch and not safe_nonlearned_intervention:665            raise ValueError(666                "Checkpoint reconfiguration_mode="667                f"{saved_reconfiguration_mode!r} does not match current "668                f"{self.reconfiguration_mode!r}."669            )670        saved_context_features = bool(671            checkpoint.get("reconfiguration_context_features", False)672        )673        if saved_context_features != self.reconfiguration_context_features:674            raise ValueError(675                "Checkpoint reconfiguration_context_features="676                f"{saved_context_features!r} does not match current "677                f"{self.reconfiguration_context_features!r}."678            )679        saved_gate_parameterization = checkpoint.get(680            "reconfiguration_gate_parameterization", "mixture"681        )682        if saved_gate_parameterization != self.reconfiguration_gate_parameterization:683            raise ValueError(684                "Checkpoint reconfiguration_gate_parameterization="685                f"{saved_gate_parameterization!r} does not match current "686                f"{self.reconfiguration_gate_parameterization!r}."687            )688        saved_task_input_mode = checkpoint.get("hatt_v20_task_input_mode", "native")689        if saved_task_input_mode != self.hatt_v20_task_input_mode:690            raise ValueError(691                "Checkpoint hatt_v20_task_input_mode="692                f"{saved_task_input_mode!r} does not match current "693                f"{self.hatt_v20_task_input_mode!r}."694            )695        self.actor.load_state_dict(checkpoint["actor"])696        if self.graph_model == self.GRAPH_LEGACY and "gat_encoder" in checkpoint:697            self.gat_encoder.load_state_dict(checkpoint["gat_encoder"])698        if (699            self.graph_model == self.GRAPH_LEGACY700            and self.mode == self.MODE_GATED701            and "actor_gat_gate" in checkpoint702        ):703            self.actor_gat_gate.data.copy_(704                checkpoint["actor_gat_gate"].to(self.device)705            )706707    def get_utility_loss(self, obs, available_actions):708        """Return auxiliary utility head loss (zero if actor has no utility head)."""709        if not hasattr(self.actor, "get_utility_loss"):710            return torch.tensor(0.0, device=self.device)711        return self.actor.get_utility_loss(obs, available_actions)712713    def get_graph_diagnostics(self):714        if not self._is_hatt_graph_model(self.graph_model):715            return {}716        result = {}717        prefix = self.graph_model718        for name, value in self.actor.last_diagnostics.items():719            if name.endswith("_attention"):720                probabilities = value.clamp_min(1e-8)721                metric = -(value * probabilities.log()).sum(dim=-2).mean()722                name = f"{name}_entropy"723            else:724                metric = value.mean()725            result[f"{prefix}/{name}"] = float(metric.cpu())726        return result727728    def get_reconfiguration_gate_regularization(self):729        """返回 actor 最近一次 evaluate_actions 产生的 gate 正则项。730731        非 hierarchical gate 或未启用 learned reconfiguration 时返回 0,732        使 trainer 可以无条件把该项接入总 loss。733        """734735        value = getattr(self.actor, "last_gate_regularization", None)736        if value is None:737            return torch.zeros((), device=self.device)738        return value739740    def get_actions(self, cent_obs, obs, rnn_states_actor, rnn_states_critic, masks, available_actions=None,741                    deterministic=False):742        """743        Compute actions and value function predictions for the given inputs.744        :param cent_obs (np.ndarray): centralized input to the critic.745        :param obs (np.ndarray): local agent inputs to the actor.746        :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor.747        :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic.748        :param masks: (np.ndarray) denotes points at which RNN states should be reset.749        :param available_actions: (np.ndarray) denotes which actions are available to agent750                                  (if None, all actions available)751        :param deterministic: (bool) whether the action should be mode of distribution or should be sampled.752753        :return values: (torch.Tensor) value function predictions.754        :return actions: (torch.Tensor) actions to take.755        :return action_log_probs: (torch.Tensor) log probabilities of chosen actions.756        :return rnn_states_actor: (torch.Tensor) updated actor network RNN states.757        :return rnn_states_critic: (torch.Tensor) updated critic network RNN states.758        """759760        obs_raw = self._reshape_obs_dict(obs, self.num_agents)761        obs_t = self._to_tensor_obs(obs_raw)762763        critic_input = self._build_critic_input(cent_obs)764        if self._is_hatt_graph_model(self.graph_model):765            actor_input = obs_t766        else:767            agent_emb, _ = self._gat_augment(obs_t)768            actor_input = self._build_actor_input(obs_t, agent_emb)769770        actions, action_log_probs, rnn_states_actor = self.actor(actor_input,771                                                                 rnn_states_actor,772                                                                 masks,773                                                                 available_actions,774                                                                 deterministic)775776        values, rnn_states_critic = self.critic(critic_input, rnn_states_critic, masks)777        gate_actions = getattr(self.actor, "last_gate_actions", None)778        if gate_actions is None:779            # 普通 Categorical、mixture、switch 都没有显式 gate_action;780            # buffer 仍存一个零占位,保持所有 graph_model 的 batch 结构一致。781            gate_actions = torch.zeros_like(actions, dtype=torch.float32)782        return (783            values,784            actions,785            action_log_probs,786            gate_actions,787            rnn_states_actor,788            rnn_states_critic,789        )790791    def get_values(self, cent_obs, rnn_states_critic, masks):792        """793        Get value function predictions.794        :param cent_obs (np.ndarray): centralized input to the critic.795        :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic.796        :param masks: (np.ndarray) denotes points at which RNN states should be reset.797798        :return values: (torch.Tensor) value function predictions.799        """800        values, _ = self.critic(self._build_critic_input(cent_obs), rnn_states_critic, masks)801        return values802803    def evaluate_actions(self, cent_obs, obs, rnn_states_actor, rnn_states_critic, action, masks,804                         available_actions=None, active_masks=None, gate_actions=None):805        """806        Get action logprobs / entropy and value function predictions for actor update.807        :param cent_obs (np.ndarray): centralized input to the critic.808        :param obs (np.ndarray): local agent inputs to the actor.809        :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor.810        :param rnn_states_critic: (np.ndarray) if critic is RNN, RNN states for critic.811        :param action: (np.ndarray) actions whose log probabilites and entropy to compute.812        :param masks: (np.ndarray) denotes points at which RNN states should be reset.813        :param available_actions: (np.ndarray) denotes which actions are available to agent814                                  (if None, all actions available)815        :param active_masks: (torch.Tensor) denotes whether an agent is active or dead.816        :param gate_actions: hierarchical gate rollout decisions, needed to replay817                             the joint action logprob during PPO update.818819        :return values: (torch.Tensor) value function predictions.820        :return action_log_probs: (torch.Tensor) log probabilities of the input actions.821        :return dist_entropy: (torch.Tensor) action distribution entropy for the given inputs.822        """823824        obs_raw = self._reshape_obs_dict(obs, self.num_agents)825        obs_t = self._to_tensor_obs(obs_raw)826827        critic_input = self._build_critic_input(cent_obs)828        if self._is_hatt_graph_model(self.graph_model):829            actor_input = obs_t830        else:831            agent_emb, _ = self._gat_augment(obs_t)832            actor_input = self._build_actor_input(obs_t, agent_emb)833834        action_log_probs, dist_entropy = self.actor.evaluate_actions(actor_input,835                                                                     rnn_states_actor,836                                                                     action,837                                                                     masks,838                                                                     available_actions,839                                                                     active_masks,840                                                                     gate_actions)841842        values, _ = self.critic(critic_input, rnn_states_critic, masks)843        return values, action_log_probs, dist_entropy844845    def act(self, obs, rnn_states_actor, masks, available_actions=None, deterministic=False):846        """847        Compute actions using the given inputs.848        :param obs (np.ndarray): local agent inputs to the actor.849        :param rnn_states_actor: (np.ndarray) if actor is RNN, RNN states for actor.850        :param masks: (np.ndarray) denotes points at which RNN states should be reset.851        :param available_actions: (np.ndarray) denotes which actions are available to agent852                                  (if None, all actions available)853        :param deterministic: (bool) whether the action should be mode of distribution or should be sampled.854        """855        obs_t = self._to_tensor_obs(856            self._reshape_obs_dict(obs, self.num_agents)857        )858        if self._is_hatt_graph_model(self.graph_model):859            actor_input = obs_t860        else:861            agent_emb, _ = self._gat_augment(obs_t)862            actor_input = self._build_actor_input(obs_t, agent_emb)863        actions, _, rnn_states_actor = self.actor(864            actor_input,865            rnn_states_actor,866            masks,867            available_actions,868            deterministic,869        )870        return actions, rnn_states_actor