delivcoal_adapter_common.py
共享 DelivCoal 配置读取与适配函数
原文件 · 98 行 · 3400 字节
experiments/reproduction/delivcoal_adapter_common.py
SHA-256: 4f0dc1aa5f2140fd115612a93fa1b64495f3fa594c331370de3d52441bcdca18
1"""CFM 和 MARL-MRT2A 复现程序共用的 DelivCoal 工具。"""23from __future__ import annotations45import json6import os7import random8from pathlib import Path9from types import SimpleNamespace1011import numpy as np12import torch13import yaml141516REPO_ROOT = Path(__file__).resolve().parents[2]17DEFAULT_CONFIG = Path(__file__).with_name("delivcoal_formal_base.yaml")181920def load_env_config(path=DEFAULT_CONFIG, **overrides):21 """读取环境配置,并把奖励文件路径转换成可直接使用的绝对路径。"""22 path = Path(path).expanduser().resolve()23 payload = yaml.safe_load(path.read_text(encoding="utf-8")) or {}24 payload.update(overrides)25 rewards = Path(payload.get("rewards_config_file", "rewards.yaml"))26 if not rewards.is_absolute():27 rewards = REPO_ROOT / rewards28 payload["rewards_config_file"] = str(rewards)29 return SimpleNamespace(**payload)303132def config_dict(config):33 return {34 key: str(value) if isinstance(value, Path) else value35 for key, value in vars(config).items()36 }373839def seed_everything(seed):40 """给 Python、NumPy 和 PyTorch 设置同一个随机种子。"""41 seed = int(seed)42 random.seed(seed)43 np.random.seed(seed)44 torch.manual_seed(seed)45 if torch.cuda.is_available():46 torch.cuda.manual_seed_all(seed)474849def atomic_json(path, payload):50 """通过临时文件更新 JSON,避免其他进程读到半份内容。"""51 path = Path(path)52 temporary = path.with_suffix(path.suffix + ".tmp")53 temporary.write_text(54 json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8"55 )56 os.replace(temporary, path)575859def summarize_episode(info, episode_reward, episode_length, sent_total):60 """把 DelivCoal 的 episode 结果整理成两种复现方法共用的指标。"""61 tasks = info.get("tasks", [])62 # DelivCoal 中类型 1 表示配送任务。这里直接使用数值,避免导入完整环境模块。63 delivery = [task for task in tasks if int(task.get("type", -1)) == 1]64 completed = sum(bool(task.get("completed")) for task in delivery)65 discovered = sum(bool(task.get("discovered")) for task in delivery)66 count = max(1, len(delivery))67 sent_goal = max(1e-8, float(info.get("sent_goal", 0.0)))68 return {69 "success": float(completed == len(delivery) and len(delivery) > 0),70 "task_completion_ratio": float(completed / count),71 "discovered_ratio": float(discovered / count),72 "sent_percentage": float(sent_total / sent_goal),73 "episode_reward": float(episode_reward),74 "episode_length": int(episode_length),75 "coalition_changes": float(76 info.get("reconfiguration", {}).get("total_coalition_changes", 0)77 ),78 "reconfiguration_decisions": float(79 info.get("reconfiguration", {}).get("total_decisions", 0)80 ),81 }828384def aggregate_rows(rows):85 """计算多次评估的均值、标准差和 95% 置信区间。"""86 if not rows:87 return {}88 result = {}89 for key in rows[0]:90 values = np.asarray([row[key] for row in rows], dtype=np.float64)91 result[key] = {92 "mean": float(values.mean()),93 "std": float(values.std()),94 "ci95": float(1.96 * values.std(ddof=1) / np.sqrt(len(values)))95 if len(values) > 196 else 0.0,97 }98 return result