返回代码索引

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