
简介一套面向多智能体强化学习与卫星调度交叉领域的实验资源基于Python与STK11联合实现适合希望入门该方向的学生或研究者也可直接用于毕业设计、课程设计或工程实训。包内提供完整工程代码其中mission.py定义任务类并随机生成经纬度等信息create_mission.py负责批量构建任务并保存为CSVcompute_access.py结合已打开的STK11场景计算各任务的可访问时段形成从任务生成到访问计算的完整闭环。资源共241个文件涵盖Python脚本、CSV数据文件、STK场景及快照文件sc/sn/sa、模型权重pth、png过程图像、说明文档等整体约79.6MB目录结构清晰便于按模块阅读与二次开发。数据集中包含lab系列与MRL_data增强数据可直接用于训练与对比实验也可根据实际需要调整任务生成规模与数据增强方式。目前已有132人浏览学习适合希望快速搭建卫星调度强化学习实验环境的学习者。1. Python与STK11融合的多智能体强化学习卫星调度实验先别谈智能先把时间窗算准拿到这个题目的人多半不是在找PPT素材而是真的有一组低轨遥感卫星要排成像任务、下传任务或者要给一套地面站网做冲突消解。问题最扎心的地方在于卫星在天上飞能对某个目标成像的时间窗口是轨道力学决定的不是算法拍脑袋决定的。传统做法是开一个整数规划或者约束规划把可见窗口作为硬约束放进去求解但任务一多、卫星一调姿态、天气一变化重调度的代价非常高。这个实验就是想用多智能体强化学习MARL让每颗卫星自己学会“什么时候该抢拍、什么时候该下传”而轨道动力学和可见性计算全部交给 STK11 来算。Python 负责智能体训练和调度决策STK11 负责提供“天时地利”的访问窗口。适合正在做卫星任务规划、应急调度编排、或者在航天仿真平台上搭 AI 算法的工程师和研究生属于典型的环境决策两层架构。2. 为什么调度离不开STK11可见性计算才是多智能体的状态来源2.1 卫星调度问题拆解约束、决策变量和奖励函数卫星调度是一个组合优化问题但用强化学习建模时必须先把它拆成马尔可夫决策过程。我的习惯是先写出“状态观测空间、动作空间、奖励函数”三张表再动代码。状态至少包含当前时间、卫星轨道位置、剩余能量、剩余存储容量、待观测目标列表、每个目标可访问的剩余时间窗。动作一般做离散化保持待命、对某个目标成像、与某个地面站建立链路开始下传。如果在细粒度做还可以加上调整滚动角或侧摆角的动作。奖励函数是这个实验的灵魂。常见做法是把“成功观测到目标”设为正向奖励 1同时惩罚能量消耗和存储占用如果违反对地成像的约束、在不可见时段尝试拍摄给出大幅度负奖励。写成 Python 风格就是def compute_reward(sat_state, action, visible_window_now): reward 0.0 done False if action IMAGE_TARGET: if not visible_window_now: reward - 5.0 # 不可见时的拍摄重罚 elif sat_state[data_free] image_data_size: reward - 2.0 # 存储不足中等惩罚 else: reward 1.0 sat_state[data_free] - image_data_size elif action DOWNLINK: if not ground_station_visible(sat_state[id], sat_state[time]): reward - 5.0 else: reward 0.8 # 下传本身也算收益因为释放了存储 return reward, done这里有一个容易被忽略的点奖励函数不能只考虑“拍到了就加分”还要考虑“拍完能不能下传”。如果只奖励成像卫星会拼命拍但存储满了以后下传窗口又不够整个调度计划就是一张废纸。我的建议是在奖励函数里把“数据到达地面站”设为最终加分项而不是把“成像动作”设为加分项——这会让智能体学到动作链而不是单步贪心。决策变量是离散动作但状态里的“可见窗口”是连续时间信息。这引出核心问题窗口从哪里来如果你自己写轨道外推和地面站覆盖计算会非常痛苦。一是地球不是标准球体J2 摄动、大气阻力影响都要建模二是地面目标点不一定是地面站可能是经纬度点需要判断俯仰角约束和太阳光照约束。STK11 就是专门干这个的它把这些算完喂给 Python 一个时间序列让智能体把精力放在策略上而不是力学复现上。2.2 STK11在链路里扮演什么角色访问计算与三维场景STKSystems Tool Kit是航天任务分析的标准工具STK11 这一代已经足够稳定。在多智能体强化学习卫星调度里STK11 不是用来训练的而是用来生成“访问时间窗”和“调度结果校验”的。具体说我需要三样东西第一每个卫星对每个地面目标的可见时间窗集合精确到秒。这是调度问题的硬约束。第二每个卫星对每个地面站的可见时间窗用于安排下传。第三场景里所有卫星的轨道星历用于在状态空间里给智能体提供当前经纬度、高度和运动方向。STK11 的Access功能把这些都算好了Python 只需要通过 COM 或 Connect 命令去调用。为什么要用 STK11 而不是开源库原因很简单开源轨道计算库能算位置但很难算“带约束的可见窗口”。比如低轨卫星对地面某一目标成像要求太阳高度角大于某个值要求星下点距离小于某个阈值要求侧摆角不超过 30°这些约束在 STK 里通过Constraints设置一次计算就能输出满足所有约束的访问区间。省下的开发时间以周为单位。这里插一句STK11 的 Python 接口不是万能胶。它提供的是命令式控制和对象模型两套 API前者通过ExecuteCommand执行字符命令后者直接操作AgStkObjectRoot。我的经验是场景构建用命令式快速读取访问间隔用对象模型方便。后面第 3 章会给出具体代码。2.3 多智能体强化学习选型Q-mix还是独立PPO多智能体强化学习在卫星调度上有两条路一条是中心化的值分解方法代表是 QMIX、QTRAN一条是每个卫星一个独立策略网络典型是 IPPOIndependent PPO如果你有大算力还可以用 MAPPO。这个实验标题既然强调“多智能体”很多初学者会直接上 QMIX但我建议从 IPPO 起步。原因有三个。第一卫星调度属于部分可观测和异步决策问题卫星之间通信有限完全中心化训练会导致联合动作空间爆炸。第二QMIX 需要把智能体的联合 Q 值分解实现复杂不说对 reward scale 很敏感。第三IPPO 的实现可以基于 Stable-Baselines3 直接写多个环境实例并行代码量少很多且对于中等规模星座比如 5~10 颗卫星已经能拿到可行解。但 IPPO 也有它的脾气每个智能体共享同样的网络结构却要处理不同的轨道参数、不同的任务队列训练时容易收敛到“各顾各”的局部策略。我的补救办法是在观测向量里加入卫星 ID 的 one-hot 编码以及当前剩余存储和电量的归一化值相当于告诉网络“我是谁、我还剩多少资源”。下面这段伪代码描述了状态构造def build_obs(sat_id, stk_timeline, task_queue, storage, energy): 从 STK11 的访问窗口和卫星状态构造观测向量 - stk_timeline: 未来两小时内每个任务的可访问标记序列 - task_queue: 当前待观测任务的优先级和截止时间 obs np.zeros(20) obs[0:len(task_queue)] [t.priority for t in task_queue[:20]] obs[10] storage / storage_capacity # 剩余存储比例 obs[11] energy / energy_capacity # 剩余能量比例 obs[12:16] one_hot[sat_id % 10] # satellite id embedding obs[16:18] stk_timeline.mean(axis0) # 下一小时可见窗口数 return obs这里的关键参数是“状态里要包含未来多少个时间步的可见窗口”。太多会把维度撑大太少又会让智能体近视。我一般取未来 1 小时步长 30 秒即 120 个时间片。如果任务密度高可以缩到 15 秒一个片但训练数据量会成倍增加。3. 从零跑通PythonSTK11最小实验建场景、导时间窗、训PPO三步走3.1 STK11的Python API连接几个必要的安装说明这个实验的前提是电脑上已经装了 STK11正版或院校授权并且安装了 Python 3.8 到 3.11。STK11 本身不依赖 Python但我们需要通过 COM 接口去驱动它。在 Windows 下先确保执行pip install pywin32这是连接 COM 的桥梁。还有一个小坑如果你的 Python 是 64 位的STK11 也必须是 64 位安装否则 Dispatch 会直接失败。检查方法很简单python -c import win32com.client; print(win32com.client.Dispatch(STK11.Application))如果这行不报错说明 COM 注册正常。连接代码可以写成import win32com.client import pythoncom def start_stk(visibleTrue, scenario_namesat_sched): # 初始化 COM 线程防止回调失败 pythoncom.CoInitialize() # 启动 STK11 应用如果本机是 STK12 则改成 STK12.Application app win32com.client.Dispatch(STK11.Application) app.Visible visible root app.Root # 新建场景或加载已有场景 root.NewScenario(scenario_name) return app, root app, root start_stk()逻辑说明pythoncom.CoInitialize()是 Windows COM 的线程初始化多线程环境里不加它后面的.Visible True可能让程序卡死。参数visible建议在调试时设为True量产训练时设为False否则每次训练都弹窗很烦。app.Root是整个 STK 场景树的根后续创建卫星、地面站、访问计算都从它开始。值得提醒的是你的 Python 版本不能太新。我遇到过 Python 3.12 下Dispatch正常但调用root.NewScenario时返回的接口对象为空大概率是 COM 类型库生成冲突换个 Python 3.10 的虚拟环境立刻就好了。3.2 构建星座场景用Python操作STK11生成卫星和地面站有了 root 对象就可以创建卫星。最稳的方式是用 STK 的 Connect Command 字符串代码可读性高以后换 STK 版本迁移也方便。下面代码创建一颗太阳同步轨道卫星和一个地面站def create_satellite_and_site(root, sat_name, site_name): # 新建卫星 cmd fNew / Satellite {sat_name} root.ExecuteCommand(cmd) # 用经典轨道要素定义轨道半长轴/偏心率/倾角近地点幅角/RAAN/平近点角 # 单位公里, 度 orbit_cmd (fSet / Satellite {sat_name} ClassicalElements f{sat_name} J2 I2000 CoordinateSystem ICRF f7000 0.001 97.5 0 0 0) root.ExecuteCommand(orbit_cmd) # 新建地面站 root.ExecuteCommand(fNew / Facility {site_name}) root.ExecuteCommand(fSet / Facility {site_name} Geodetic 39.9 116.4 0)参数说明半长轴 7000 公里大约是 350 公里轨道高度地球半径 6378 km偏心率 0.001 接近圆轨道倾角 97.5° 是常见的太阳同步轨道倾角。J2表示考虑地球扁率摄动I2000是惯性坐标系。Set / Facility后的三个数字是纬度、经度、高度公里上面设置的是北京站。这一步是后面一切计算的地基。要确认卫星真的在天上飞可以在 STK 窗口看到二维地图上的星下点轨迹。如果没有轨迹检查半长轴是否小于地球半径或者轨道命令语法有没有被 STK 吞掉错误。建议手动把visibleTrue打开跑一遍别一上来就隐藏窗口这不是玄学是你需要肉眼确认场景是否成立。3.3 用STK11批量计算可见时间窗并导出场景建好后下一步是计算卫星对地面站和地面目标的访问窗口。地面目标同样可以建模为Facility或者用GeoPoint。访问计算先看成两两之间算可见区间代码如下def get_access_windows(root, sat_name, target_name, time_start, time_stop): # 获取两个对象 sat root.GetObjectFromPath(fSatellite/{sat_name}) tgt root.GetObjectFromPath(fFacility/{target_name}) # 创建访问计算对象 access sat.GetAccessToObject(tgt) # 设置访问时间范围 access.StartTime time_start access.StopTime time_stop # 执行计算 access.ComputeAccess() windows [] intervals access.TimeIntervals for i in range(intervals.Count): interval intervals.Item(i) windows.append((str(interval.StartTime), str(interval.StopTime))) return windows windows get_access_windows(root, Sat1, Beijing, 1 Jan 2025 00:00:00.000, 2 Jan 2025 00:00:00.000)参数说明StartTime和StopTime用 STK 的时间格式字符串支持1 Jan 2025 00:00:00.000。Access.ComputeAccess()会在 STK 内部完成传播和解算这一步可能耗时数十秒取决于场景复杂度和精度。TimeIntervals是一个集合每个元素包含开始和结束时间单位是 UTC 秒级。这段代码是整个实验最值得保存的部分。我一般会把所有卫星和任务的访问窗口统一算完写进一个access_table.csv字段包括卫星ID、目标ID、开始秒、结束秒、可见时长。后面训练时直接读 CSV不需要再碰 STK。这可以避免训练循环里反复调用 COM 导致的内存泄漏。import csv def export_access_table(root, sat_list, target_list, time_start, time_stop, csv_path): with open(csv_path, w, newline) as f: writer csv.writer(f) writer.writerow([sat, target, start, stop]) for sat in sat_list: for tgt in target_list: for start, stop in get_access_windows(root, sat, tgt, time_start, time_stop): writer.writerow([sat, tgt, start, stop])这个 CSV 是 RL 环境的输入。注意STK 返回的时间字符串直接转成时间戳时要统一格式。我建议在 STK 里先把场景的时间系统设为 UTC再用datetime.strptime解析避免丢秒。3.4 智能体训练循环状态、动作、奖励怎么接进PPO环境构建我习惯继承gym.Env每个卫星独立一个环境实例但共享一个全局时间轴。这样训练代码可以套用现成的 PPO 实现。为了控制篇幅这里给出一个可运行的最小骨架import gym from gym import spaces import numpy as np from stable_baselines3 import PPO class SatelliteScheduleEnv(gym.Env): def __init__(self, access_csv, sat_args): super().__init__() self.access_windows load_access_csv(access_csv) # 按秒存成列表 self.action_space spaces.Discrete(3) self.observation_space spaces.Box(low0, high1, shape(20,), dtypenp.float32) self.sat_id sat_args[sat_id] self.time_step 30 # 每步30秒 self.max_steps 240 self.current_step 0 self.data_free 1.0 def step(self, action): # 判断当前时刻是否处于可见窗口 now_sec self.current_step * self.time_step visible any(win[0] now_sec win[1] for win in self.access_windows[self.sat_id]) # 奖励函数见 2.1 reward, done compute_reward_standalone(action, visible, self.data_free) self.data_free max(0, self.data_free - 0.02 if action 1 else self.data_free 0.05) self.current_step 1 done self.current_step self.max_steps obs build_obs(self.sat_id, self.access_windows[self.sat_id], now_sec, self.data_free) return obs, reward, done, {} def reset(self): self.current_step 0 self.data_free 1.0 return build_obs(self.sat_id, self.access_windows[self.sat_id], 0, self.data_free)训练代码# 每个卫星一个环境可以将多个环境传入并合并或用同一环境训练多个卫星 env SatelliteScheduleEnv(access_table.csv, {sat_id: 0}) model PPO(MlpPolicy, env, learning_rate3e-4, gamma0.99, n_steps2048, batch_size128, verbose1) model.learn(total_timesteps200_000) model.save(sat0_ppo)参数说明learning_rate3e-4是 PPO 默认值对于稀疏奖励场景不建议更大gamma0.99平衡短期和长期收益n_steps2048表示每次收集 2048 个 step 后做一次更新这个值越大越稳定但内存占用越高。batch_size128控制梯度更新子集大小如果训练不稳定可以调到 64。action1和action2对应成像和下传激活后要结合可见性给奖惩。这里的坑是环境对象只代表单个卫星但多智能体训练需要并行多个SatelliteScheduleEnv并且共享时间步。常见做法是写一个MultiSatEnv内部实例化多个子环境每步先让所有子环境执行动作再统一推进 time_step。Stable-Baselines3 本身不原生支持多智能体可以转成多输入单输出拼合或者直接用 Ray/RLlib 的PPOTrainer。如果你只是想验证可行性先对每颗卫星单独训练再把策略拼起来跑 STK 回放也不失为一条捷径。4. 避坑指南STK11与Python融合的5个坑4.1 现象python和STK11连不上Dispatch报错“无效的类字符串”原因最常见的是 STK11 没有用“以管理员身份安装”的 COM 注册机制或者 Python 是 32/64 位与 STK 不一致。还有可能你装的不是完整版 STK而是某个独立模块COM 服务未注册。解决先用管理员身份打开 STK11让它完成首次注册然后重启 Python。再不行用regsvr32重新注册 STK 安装目录下的 COM 组件。最后检查 Python 位数python -c import platform; print(platform.architecture())必须是 64 位。这个坑 90% 是我第一批实验的朋友遇到的所以把它排第一。4.2 现象STK仿真运行太慢训练一天只走几千步原因训练循环里反复调用STK Desktop的界面刷新和 COM 回调。每GetAccessToObject一次可能触发场景重算再加上三维场景渲染性能消耗极大。解决把“STK 计算访问窗口”和“RL 训练”拆开。先离线跑完所有窗口存成 CSV 或 numpy 文件训练时不再连接 STK而是读缓存。这相当于把 STK11 当成一次性的“数据生成器”。如果需要频繁更新轨道每 100 个训练 episode 才重新调用一次 STK 更新窗口而不是每步调用。我通常把一个 10 颗卫星 50 个目标的双日场景算窗控制在 5 分钟内之后训练完全不碰 STK。4.3 现象多智能体奖励稀疏PPO 训练不收敛动作变成全“待命”原因奖励正样本太少智能体一开始随机策略几乎碰不到成像窗口所以它学到了“什么都不做”这个局部最优。这和卫星调度场景高度相关可见窗口很短动作空间却很大。解决奖励塑形。除了最终成功成像给出“接近可见窗口”的中间奖励比如当目标即将进入可见窗口时给一个小的正奖励促使智能体做好准备当目标离开窗口后才到达给一个小惩罚。另外把动作空间改成“只对当前处于可见窗口内的目标成像”可以消除大量无效动作。另一种办法是采用课程学习先简化成只有一颗卫星和一个地面站等 PPO 能稳定接住任务再扩展到多星多任务。4.4 现象调度结果违反可见时间窗约束卫星“瞎拍”原因你把 STK 算出的窗口转成连续时间段后在 RL 环境里以 30 秒步长做离散判断可能漏掉边界上的秒级窗口。比如一个可见窗口只有 25 秒步长 30 秒两个相邻决策点正好跳过窗口智能体永远学不到“在这个窗口行动”。解决这种事我称之为“时间刻度玄学”。把时间步长缩小到可见窗口最小时长的 1/3 以下或者把动作允许时间放宽只要决策时刻与窗口有重叠就允许动作生效但奖励按重叠时长加权。另一个更稳的兜底是训练完成后的调度计划重新交给 STK11 做一次约束验证凡是落在窗口外的任务直接丢弃绝不让模型自己去“补”。4.5 现象从STK导出的坐标和姿态数据坐标系不一致指向误差大原因STK 内部默认使用 TEME 或 J2000 惯性系而地面目标使用的是地固系ECEF。你在构建状态时如果直接用卫星的惯性系位置和地面站经纬度做距离判断结果可能完全错位这已经不是强化学习的问题而是工程坐标系问题。解决在 STK 对象模型中强制为每个对象设置ReferenceFrame。读取卫星位置时指定Earth Fixed坐标系这样和地面站经纬度直接兼容。在 Python 里转换也可以但我懒得背转换矩阵所以直接让 STK 输出同一坐标系的值。代码里可以加一句sat_position sat.VO.Vector.DataType(Cartesian) frame sat_position.Resultant.ReferenceFrame # 输出时设置成 Earth Fixed cmd SetUnits / CoordinateSystem Earth Fixed root.ExecuteCommand(cmd)虽然这行命令不能完全保证所有输出统一但在 STK 的场景属性里把“位置显示坐标系”改成 Earth Fixed绝大多数问题能迎刃而解。5. 进阶技巧用STK的评估报告验证调度计划以及并行仿真加速5.1 用STK11的Report验证调度计划是否有“水分”训练完的策略不能只看 reward converge 就宣告成功。我会把每个卫星的调度序列导成任务列表再交给 STK 的Access和Coverage功能做回放。具体方法是在策略训练结束后生成一个schedule.csv里面是每颗卫星在某个时间段的动作时间戳然后回到 STK 场景中在地面目标的约束里设置一个全局要求让 STK 输出每个任务是否满足可见性和能源约束。STK 的Report Manager里有一个Access Summary Report可以看到每个目标实际被访问的时间长度和我的调度计划时间戳做差超过 5 秒误差的就得回头调时间步长。这个验证成本很低却能避免你在答辩或项目评审时被质疑“这是仿真里自嗨”。5.2 并行仿真加速要么多进程要么放弃COMCOM 调用 STK 不是线程安全的用 Python 的ThreadPoolExecutor并发连接同一个 STK 实例大概率会把 STK 搞崩。我踩过这个坑后来改成两种方案一种是开多个 STK 实例每个进程独立创建场景用 Pythonmultiprocessing分别跑不同星座的窗口计算另一种是彻底离线化把 STK 导出好的访问窗口缓存到共享目录训练进程只读 cache。对于多智能体采样Stable-Baselines3 的并行向量环境SubprocVecEnv可以同时跑多个SatelliteScheduleEnv但每个环境内部不连 STK只做静态表查询。如果你的实验需要实时对应新的轨道机动那就应该用实时建场景计算窗口的专用服务而不是在训练循环里插 STK 调用。关于并行的一点个人习惯我会在代码里保留一个cache_version字段场景参数一改版本号一变所有缓存全部失效否则你会经常对着旧时间窗训练新策略结果越训越差还不自知。这套实验做下来我最强烈的感受是强化学习在这里并不神秘它只是负责在 STK11 给定的可能性空间里寻找调度策略而 STK11 也不神秘它就是把“什么时候能看到”这个事实算得明明白白。把两件事都不当黑匣子你的实验就能反哺给一线调度人员。希望这段基于真实踩坑的复盘能帮到你。本文还有配套的精品资源点击获取