☰
AGV调度系统仿真平台详解:从建模到调度算法落地
2026/9/25 5:17:19 网站建设 项目流程

简介:AGV调度系统的仿真平台完整源码与项目说明,面向计算机、数学、电子信息等专业课程设计、期末大作业与毕业设计场景。压缩包共2000个文件,大小14.92MB,其中1525个JavaScript文件承担前端界面与仿真逻辑,289个Markdown文档提供项目说明与开发笔记,160个JSON用于配置数据,辅以少量HTML、XML、Python等文件,便于直接运行与二次调试。该资源已有785人浏览学习,源码可直接下载使用。项目说明与源码配合,能帮助读者理解AGV调度的整体架构、任务流程及关键实现思路;适合具备一定编程基础、乐于钻研调度算法的学习者,可参考分模块结构快速定位功能,再结合实际需求扩展或改造,是学习智能仓储与自动导引车调度仿真的实用资料。

1. AGV调度系统的仿真平台是什么:它对谁有用,解决什么问题

AGV调度系统是仓储和工厂自动化里的“大脑”——负责给一组 AGV 分配搬运任务、规划路径、控制路口防撞。仿真平台,就是把这套调度逻辑放进虚拟工厂里跑起来的地方。这类“源码+项目说明.zip”的包,通常包含仿真引擎、调度算法实现、可视化界面和一份项目说明文档,让你在没有真车、没有场地的情况下,先把调度逻辑验证一遍。适合三类人:做课程设计或毕业设计的在校生,刚接手 AGV 项目的自动化工程师,以及需要评估调度方案效果的实施人员。核心价值一句话:把“调度能不能跑通、会不会堵死、效率多高”从真机搬到电脑里,省掉大量调硬件的时间。下面按我自己的拆包和复现习惯,把这类平台从建模到跑通讲清楚。

2. 仿真平台的核心组成怎么建模:地图、AGV 实体与任务引擎

拿到源码先别急着点运行。AGV 调度仿真平台不管用 Python、Java 还是 C++ 写,模块划分都逃不开四块:地图与实体建模、任务引擎、调度算法、仿真主循环。前两块是数据基础,后两块是逻辑核心。把这几层的关系理清楚,读代码时就不会迷路。

我一般会按“数据从哪来 → 数据怎么被更新 → 算法在哪个环节介入”的顺序去读。地图决定 AGV 能走哪,任务引擎决定要干什么,调度算法决定派谁去、走哪条路,主循环把这一切按时间轴推起来。下面逐个展开。

2.1 栅格地图与站点建模:把物理场地翻译成调度器能读的图

仿真里的地图最常见的是两种表示:栅格地图和拓扑图。栅格地图用二维数组存,每个格子记一个值,路径规划直接用 A* 这类搜索算法;拓扑图用节点和边表示路口与路段,适合道路结构清晰的厂区。课程项目和入门源码绝大多数用栅格,因为直观、好调试。

栅格地图的格子取值约定很关键,各套源码不统一,最常见的约定是:0 表示可通行,1 表示障碍,2 表示站点或充电位。读地图文件时,要把站点单独抽出来存成列表,同时把格子改回 0,否则路径规划会把站点当成障碍物。

# map_loader.py - 读取栅格地图文件,0=通道 1=障碍 2=站点 import numpy as np def load_map(path: str): raw = np.loadtxt(path, dtype=int, delimiter=",") # 站点层和通行层拆开:站点格子在规划时按可通行处理 grid = np.where(raw == 2, 0, raw).astype(np.int8) stations = [(int(r), int(c)) for r, c in zip(*np.where(raw == 2))] return grid, stations

这里有个新手最容易忽略的约定:np.where(raw == 2, 0, raw)把站点格改成 0,是因为 AGV 本身要能开进站点装卸货。如果站点也算障碍,规划器永远不会把站点放进路径里,AGV 到了站点门口就停下来,任务永远完不成。这类“站点不可达”的 bug 在仿真里非常隐蔽,因为报错不是崩溃,而是吞吐量异常低。

栅格地图还有一个参数容易被丢掉:cell_size_m,即每个格子代表的实际边长。算法里算出来的是“格子数”,要换算成米才能和真实 AGV 速度、任务距离对齐。很多源码在仿真内部只用格子数,最后发现仿真时间对不上真机,就是少了这个换算系数。

2.2 AGV 实体模型:速度、载重、电量与状态机

仿真里的 AGV 不是地图上一个会动的点,它是有状态的实体。状态机是 AGV 模型的核心,常见状态包括:空闲(IDLE)、已分配待命(RESERVED)、移动中(MOVING)、装货中(LOADING)、卸货中(UNLOADING)、充电中(CHARGING)、故障(FAULT)。调度算法实质上就是在驱动这些状态之间的合法跳转。

# agv.py - 仿真用 AGV 实体,状态机驱动 from enum import Enum class AGVState(Enum): IDLE = 0 RESERVED = 1 # 已分配任务,正在等待路径 MOVING = 2 LOADING = 3 UNLOADING = 4 CHARGING = 5 FAULT = 6 class AGV: def __init__(self, agv_id, speed_mps=1.0, capacity_kg=500, battery_capacity=100.0): self.agv_id = agv_id self.speed_mps = speed_mps # 空载速度,满载可单独拆 self.capacity_kg = capacity_kg self.battery_capacity = battery_capacity self.battery = battery_capacity self.state = AGVState.IDLE self.pos = None # (row, col) self.path = [] # 规划出的路径点序列 self.task = None # 当前任务对象 def can_accept(self, task_weight_kg) -> bool: return (self.state == AGVState.IDLE and task_weight_kg <= self.capacity_kg and self.battery > 0.2 * self.battery_capacity)

can_accept里的 0.2 是电量安全阈值,低于这个值不接新任务,应该去充电。这个阈值在正式项目里会单独做成配置项,而不是写死在代码里。载重检查也放在这一层,避免派单模块把超重任务分给小车。

注意空载和满载速度的差别:现实中 AGV 载重后加速变慢、转弯更小心,但很多课程源码只用一个speed_mps统一算。这么简化在仿真里看不出问题,一旦要和真机做数据比对,这个误差就藏不住了。我的做法是:模型里预留speed_empty和speed_loaded两个字段,初期都填同一个值,后面按真机参数校准。

2.3 任务引擎:订单生成、优先级与仿真主循环

任务模型通常是一个数据类,包含任务 ID、取货点、送货点、重量、优先级、创建时间、状态,以及被分配给的 AGV ID。任务状态从 PENDING 到 ASSIGNED 再到 EXECUTING,最终落到 DONE 或 FAILED。这个状态流转要和 AGV 的状态机严格对应,后面避坑章节会专门讲这里翻车的案例。

任务生成器一般支持两种模式:按泊松过程随机到达,或从 CSV 文件按固定时间表导入。泊松到达适合做压力测试和算法对比,固定时间表适合复现业务场景。

# task.py - 任务数据模型与泊松到达生成器 import random from dataclasses import dataclass from enum import Enum class TaskState(Enum): PENDING = 0 ASSIGNED = 1 EXECUTING = 2 DONE = 3 FAILED = 4 @dataclass class Task: task_id: int pickup: tuple # (row, col) dropoff: tuple weight_kg: float = 100.0 priority: int = 0 # 0 普通, 1 加急 state: TaskState = TaskState.PENDING agv_id: int = -1 create_time: float = 0.0 def poisson_task_generator(rate_per_hour, map_size, duration_s, seed=42): """按泊松过程生成任务序列,rate 为每小时任务数""" rng = random.Random(seed) interval = 3600.0 / rate_per_hour t = 0.0 task_id = 0 while t < duration_s: t += rng.expovariate(1.0 / interval) yield Task(task_id=task_id, pickup=(rng.randrange(map_size), rng.randrange(map_size)), dropoff=(rng.randrange(map_size), rng.randrange(map_size)), create_time=t) task_id += 1

泊松过程的两句参数值得解释:interval是平均任务间隔秒数,rng.expovariate(1.0 / interval)生成指数分布的间隔,这样任务到达是随机的但长期平均速率稳定。seed=42是后悔药——固定随机种子后,每次实验的任务序列完全一致,算法对比才有意义。不固定 seed 的仿真对比,结果基本靠玄学。

仿真主循环是整套平台的发动机,最常见的是定步长推进:

# sim_engine.py - 仿真主循环骨架 def run_simulation(cfg, world): sim_time = 0.0 task_gen = poisson_task_generator(cfg["tasks"]["rate_per_hour"], cfg["map"]["size"], cfg["tasks"]["duration_s"]) while sim_time < cfg["tasks"]["duration_s"]: # 1. 把当前时刻到达的新任务加入待派队列 for task in task_gen: if task.create_time > sim_time: break pending_queue.append(task) # 2. 对每个空闲 AGV 尝试派单 # 3. 对已派单的 AGV 做路径规划与预留 # 4. 按 time_step 推进所有 MOVING 状态的 AGV 位置 # 5. 检查是否到站、装卸货是否完成、是否触发充电 # 6. 记录事件日志 sim_time += cfg["simulation"]["time_step_s"]

定步长的优点是实现简单、状态更新整齐;缺点是步长太大时 AGV 会“瞬移”,撞穿障碍物。步长一般取 0.2~0.5 秒,和 AGV 速度、栅格尺寸配合着调。事件驱动仿真更精确但实现复杂,入门源码基本不碰,能看懂定步长主循环,就已经能读明白大部分 AGV 仿真项目了。

3. 调度算法怎么落地:A* 路径规划、死锁处理与派单策略

地图和实体模型搭好后,真正决定仿真平台价值的,是调度算法怎么写。这一章的选择直接影响后面跑出来的吞吐量和死锁率。算法这部分,我的原则是:先跑通最朴素的版本,再一步步加约束,不要一上来就上强化学习或者混合整数规划那种重武器。

3.1 A* 路径规划与预留表:先到先得不是好策略

单台 AGV 的路径规划,A* 是绝对的主流。栅格地图上启发式用曼哈顿距离就行,因为 AGV 只有上下左右四个移动方向。但多台 AGV 同时跑的时候,问题立刻出现:每台车各自规划的最短路径,合在一起就是碰撞和死锁。

常见的解法是预留表(Reservation Table)——每台 AGV 规划完成后,把自己将要占用的格子按时间段登记进去,后续 AGV 规划时避开这些时空冲突。这是一种“先到先得”的机制,实现简单,效果立竿见影。

# astar.py - 带预留表检查的 A* 路径规划 import heapq def astar_with_reservation(grid, start, goal, reservation, time_step=1.0): """reservation: dict[(row, col)] -> [(t_start, t_end, agv_id)]""" rows, cols = grid.shape open_heap = [(0, 0, start, [])] # (f, g, pos, path) best_g = {start: 0} while open_heap: f, g, pos, path = heapq.heappop(open_heap) if pos == goal: return path + [pos] for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]: nxt = (pos[0] + dr, pos[1] + dc) if not (0 <= nxt[0] < rows and 0 <= nxt[1] < cols): continue if grid[nxt[0], nxt[1]] == 1: continue arrive_t = len(path) * time_step if _conflict(nxt, arrive_t, reservation): continue ng = g + 1 if ng < best_g.get(nxt, float('inf')): best_g[nxt] = ng nf = ng + abs(nxt[0] - goal[0]) + abs(nxt[1] - goal[1]) heapq.heappush(open_heap, (nf, ng, nxt, path + [pos])) return None def _conflict(cell, t, reservation): for t0, t1, _ in reservation.get(cell, []): if t0 <= t < t1: return True return False

这个实现里最值得注意的是arrive_t = len(path) * time_step——到达某个格子的事件,是从起点出发走过的步数乘以单步耗时。预留表里每一条记录是“哪台车、从什么时间到什么时间占用哪个格子”。_conflict只判断时间区间是否重叠,不判断是不是同一台车,因为自己预留过的格子自己再走一遍是允许的。

一个常见简化是:预留表只按格子记,不区分方向。这会导致两台 AGV 同向跟车时被误判为冲突,现实中前车走了后车可以跟着走。想做得细一点,就把预留记录加上方向字段,同向同格不冲突,对向才冲突。初学阶段先不加,但要知道这个边界在哪。

3.2 死锁检测与避免策略:环路、互等和单行道

死锁是 AGV 调度里最经典也最折磨人的问题。教科书上的场景:两车在窄通道对向相遇,谁也没法倒车;四车围成一个环,每台车都等着前面的车让位。仿真里死锁的可怕之处在于它通常不是必现的,而是任务量到了一定阈值才冒出来。

处理死锁有两条路线:避免和检测。避免的思路是提前阻止可能形成环路的资源分配,比如单向通道、进入关键路段前先锁定整段路;检测的思路是运行时构建等待图,发现环就强制解决。生产系统两条腿走路,入门源码一般只做检测。

# deadlock.py - 基于等待图的死锁检测(拓扑排序判环) from collections import deque def detect_deadlock(wait_for: dict): """wait_for: {agv_id: [它等待的agv_id列表]},返回环上的agv_id集合""" indeg = {k: 0 for k in wait_for} for waits in wait_for.values(): for w in waits: indeg[w] = indeg.get(w, 0) + 1 q = deque([k for k, d in indeg.items() if d == 0]) visited = set(q) while q: node = q.popleft() for w in wait_for.get(node, []): indeg[w] -= 1 if indeg[w] == 0 and w not in visited: visited.add(w) q.append(w) return set(indeg.keys()) - visited

等待图的意思是:如果 AGV A 被 AGV B 挡住了,就记一条 A 等待 B 的边。拓扑排序后,所有没进队列的节点就是环上的成员。检测出环后,最粗暴的解法是挑环上优先级最低的一台车,取消它的路径,让它退到旁边的待避点,过几秒重新规划。这就是所谓的“后退一步海阔天空”。

我的经验是:检测代码写起来容易,难的是“检测出来之后怎么办”。如果只是单纯停住重规划,两台对向而行的车会反复互等,形成活锁。至少要加一个随机退避时间,或者让其中一台车绕路。在仿真平台里验证死锁策略,比在真机上验证便宜得多——这也是仿真的核心价值之一。

3.3 派单策略:贪心、轮询与拍卖的取舍

路径规划解决“怎么走”,派单解决“派谁去”。最朴素的策略是贪心:每次有任务,在所有空闲 AGV 里挑距离取货点最近的一台。实现简单,但会带来一个明显问题——离得近的车永远被派,远的车一直闲着,电量消耗极不均匀。

改进方向有两个:一个是在代价函数里加惩罚项,比如电量低于某个阈值就加大空驶距离的权重;另一个是换策略,轮询保证公平,拍卖让每台车自己报价。

# dispatcher.py - 最小代价派单,代价 = 空驶距离 + 电量惩罚 def dispatch_task(task, idle_agvs, grid): best = None best_cost = float('inf') for agv in idle_agvs: path = astar_with_reservation(grid, agv.pos, task.pickup, {}) if path is None: continue dist = len(path) # 电量低于30%的车,空驶代价上浮30%,尽量留给充电 penalty = 1.0 if agv.battery > 0.3 * agv.battery_capacity else 1.3 cost = dist * penalty if cost < best_cost: best_cost = cost best = (agv, path) return best

注意这里astar_with_reservation传的是空预留表,因为派单阶段只想估算距离,不想让预留表影响“谁更近”这个判断。真正执行时再带着完整预留表重新规划一次。电量惩罚系数 1.3 是经验值,取值太大会导致电量低的车永远接不到活,太小又起不到均衡作用,需要根据仿真数据调。

三种派单策略的取舍,我用这个表概括:

策略吞吐量电量均衡实现成本适用场景
贪心最近距离高差低任务稀疏、电量充足
轮询低好低演示用,实际很少单用
代价拍卖中高中中大多数项目的主力方案

拍卖机制和上面的代价函数本质是一回事,区别只是把“调度器统一算代价”变成“每台车自己报代价”,模型上更灵活,可以加进对电量、距离、任务优先级的不同权重。入门阶段建议直接做“调度器统一计算代价”,把权重系数做成配置,等需要扩展再改成拍卖。

4. 把仿真源码跑通:目录结构、最小配置与参数调优

读代码和跑代码是两回事。拿到“源码+项目说明.zip”这类包,我一般先花十分钟做三件事:看 README、看 requirements 或 pom.xml、看配置目录。这三样能告诉你项目用什么语言、依赖什么库、怎么改参数,比闷头翻 src 快得多。

4.1 zip 解压后的目录结构与运行环境

这类源码包的目录结构大同小异,一个规范的仿真项目通常会长这样:

agv_simulation/ ├── README.md # 项目说明,先读这个 ├── requirements.txt # Python 依赖清单 ├── config/ │ └── scenario.yaml # 场景配置 ├── data/ │ ├── map_01.txt # 地图文件 │ └── tasks_01.csv # 固定任务表(可选) ├── src/ │ ├── core/ # 地图、AGV、任务模型 │ ├── planner/ # 路径规划与预留 │ ├── dispatcher/ # 派单模块 │ └── sim_engine.py # 仿真主循环 ├── viz/ │ └── viewer.py # 可视化 └── results/ └── logs/ # 仿真日志输出

如果包是 Java 写的,requirements.txt会换成pom.xml或build.gradle;C++ 则是 CMakeLists。不管什么语言,core、planner、dispatcher这三个模块的职责划分基本不变。我在读新项目时,会先画一遍这三层的数据流:core 提供实体和地图,planner 算出路径,dispatcher 决定任务归属,主循环把它们串起来。

运行环境上,Python 版最常见的坑是 numpy、matplotlib 版本冲突。建议新建虚拟环境再装依赖,不要直接往系统 Python 里灌包。项目说明里如果写了 Python 版本要求,比如 3.8 或 3.10,就按它来,省得后面踩兼容坑。

4.2 最小场景配置:5 台 AGV 一小时跑通

先把场景缩到最小:5 台 AGV、一张 30×30 的栅格图、平均每小时 120 个任务、仿真时长 3600 秒。配置通常写成 YAML 或 JSON,改起来不用动代码。

# config/scenario.yaml - 最小场景配置 map: file: "data/map_01.txt" cell_size_m: 0.5 # 每个栅格代表的实际尺寸(m) fleet: count: 5 speed_mps: 1.0 # 空载速度(m/s) capacity_kg: 300 charge_threshold: 0.2 # 电量低于20%触发充电 tasks: rate_per_hour: 120 # 平均每小时120个任务(泊松到达) duration_s: 3600 # 仿真时长1小时 simulation: time_step_s: 0.5 # 仿真步长(秒) enable_visualization: false

跑起来的命令很简单:

pip install -r requirements.txt python src/sim_engine.py --config config/scenario.yaml

配置里几个参数的作用先说清楚:cell_size_m决定一个格子代表多大,它影响路径长度和真实物理时间的换算;charge_threshold是充电触发阈值,设高了 AGV 频繁去充电,设低了容易电量耗尽趴窝;time_step_s是主循环步长,越小越精确但越慢。第一次跑通建议把enable_visualization关掉,先看日志输出,确认没有报错再开可视化。

跑通后第一件事不是看吞吐量,而是看日志里有没有FAILED状态的任务和死锁事件。如果没有,说明基础链路是通的;如果有,先别急着调参,按下一章的排查思路走。

4.3 必调参数与日志怎么看

参数调优是仿真里最容易“玄学”的环节。我的建议是:一次只动一个参数,其他全部固定,并且每次对比都用相同的随机种子。否则你根本分不清吞吐量变化是参数引起的,还是任务序列随机波动引起的。

仿真日志的输出格式,我习惯用 JSON 一行一条事件,方便后续用 pandas 分析。

# sim_engine.py 里常用的结构化日志输出 import json def log_event(clock, event_type, **kw): line = {"t": round(clock, 2), "event": event_type} line.update(kw) print(json.dumps(line, ensure_ascii=False))

输出长这样:

{"t": 12.5, "event": "task_assigned", "task_id": 3, "agv_id": 1} {"t": 45.0, "event": "task_done", "task_id": 3, "agv_id": 1, "duration": 32.5} {"t": 78.5, "event": "task_failed", "task_id": 7, "agv_id": 4, "reason": "deadlock_timeout"} {"t": 90.0, "event": "agv_charging", "agv_id": 2, "battery": 0.18}

看日志有个技巧:先统计task_failed事件,如果失败率高,大概率是死锁或路径不可达,而不是参数问题。再看agv_charging事件的时间分布,如果多台车在同一时间段集中充电,说明电量阈值设置让车队“同频共振”了,要错开阈值。task_done的平均耗时才是调优的核心指标,看它的趋势,而不是看单条。

排障时最实用的参数顺序:先调time_step_s,再调rate_per_hour,最后才动调度算法里的权重系数。前两个是环境参数,错了会让结果失真;后面是策略参数,错了只会让效率高低不同。

5. 仿真平台避坑记录:五条反复让人翻车的真实现象

仿真平台写出来容易,想让它稳定、可信、不翻车,得靠踩坑喂出来。下面这几条是我在跑 AGV 调度仿真时见过最多的坑,按“现象 → 原因 → 解决”写,每条都能直接复现。

5.1 仿真越跑越慢,内存一路涨

现象:仿真跑到半小时后,每一秒的仿真时间明显变慢,内存占用持续上升,最后卡死。

原因:日志事件全部攒在内存列表里没有落盘;预留表里已经过去的记录从未清理,越积越多;路径规划结果存了全量历史没有释放。

解决:日志每满 1000 条就 flush 到文件,内存里只留最近一段;主循环每个步长结束时清理预留表里t_end < 当前时间的记录。这是性能问题里最容易忽略的一条,我见过好几次因为预留表不清理导致仿真跑不完一小时的案例。

5.2 AGV 到站后原地打转,任务完成了车不释放

现象:AGV 走到卸货点,任务状态已经是 DONE,但车还停在原地,或者还在沿原路径来回走,后面的任务接不上。

原因:任务状态转到 DONE 的代码分支里,忘了把 AGV 状态重置为 IDLE、清空agv.path;预留表的释放写在了另一个函数里,因为某个异常分支跳过了它。

解决:把“任务完成”和“AGV 重置”绑在同一个函数里处理,重置动作包括:状态置 IDLE、路径清空、预留表释放、当前任务置空。写完加一条断言:任务 DONE 后 AGV 的path必须为空。这条坑基本是每个新手都会踩一遍的。

5.3 死锁只在 AGV 数量超过 8 台后出现,少一台就没事

现象:5 台 AGV 跑得稳稳当当,加到 9 台后频繁报死锁,任务失败率飙升。

原因:两台或多台 AGV 在同一时刻独立规划路径,彼此不知道对方刚预留的路线,形成“同时决策”的冲突。车少的时候路径空间宽裕,碰不上;车一多,冲突概率就上来了。

解决:给派单和规划加一个顺序——按 AGV 编号从小到大依次规划,后面规划的车带着前面车刚写入的预留表重新算路。这就是所谓的“顺序规划”,代价是响应变慢一点点,但死锁率大幅下降。这也是为什么仿真平台里死锁的排查思路里,第一件事就是看规划是否带预留表、是否按顺序执行。

5.4 充电任务让所有 AGV 同时趴窝

现象:仿真跑到中段,所有 AGV 都去充电,整个任务队列清空,吞吐量瞬间归零。

原因:所有 AGV 用同一个电量阈值(比如 0.2),同一时刻耗尽、同一时刻触发充电;而充电站只有一两个,后面的车排队等。

解决:给每台 AGV 的电量阈值加偏移,比如0.15 + 0.02 * agv_id,让触发时间错开;充电站加排队机制,排不到就停在待避区,不要堵在充电站门口。这条在真机项目里一样成立,只是真机上你能看见车堆在充电站旁边,仿真里只能看到吞吐量曲线塌下去。

5.5 仿真结果和真机跑出来的数据完全对不上

现象:仿真显示每小时能完成 200 个任务,真机验收只有 100 个,差距大得没法解释。

原因:仿真模型少了加减速、转弯耗时、装卸货时间;或者栅格cell_size_m设置和实际场地不一致,导致路径长度算短了。

解决:先补常数耗时,比如装货 10 秒、卸货 8 秒、转弯 2 秒;再把 AGV 从静止到全速的时间按匀加速近似。校准方式是拿一台真车跑一条已知长度的路径,测实际耗时,反推模型里缺了哪部分。记住一句话:仿真结果的绝对值别当真,相对值(A 策略比 B 策略快多少)才是可用的。

6. 把仿真结果做成可验收产出:指标口径、可视化回放与回归基线

仿真平台跑通、参数调完,最后一步是把结果变成能汇报、能对比、能复用的东西。我自己的习惯是定三套东西:指标口径、回放工具、回归基线。这三样齐全,仿真平台才真正有说服力。

6.1 三个必看指标怎么统计

吞吐量(每小时完成任务数)、平均任务完成时长、AGV 平均利用率,这三项是任何 AGV 调度方案汇报里都跑不掉的。统计口径要固定:任务完成时长是从创建时间算,还是从分配时间算?我统一用创建时间,因为派单等待也是调度质量的组成部分。AGV 利用率 = 非空闲时间 / 仿真总时长,注意充电时间算不算工作,两种口径各有道理,但要在报告里写清楚。

# metrics.py - 从日志统计核心指标 import json def compute_metrics(log_lines): total = done = 0 duration_sum = 0.0 busy_seconds = {} for line in log_lines: ev = json.loads(line) if ev["event"] == "task_done": total += 1 done += 1 duration_sum += ev["duration"] busy_seconds.setdefault(ev["agv_id"], 0.0) elif ev["event"] == "task_failed": total += 1 return { "throughput_per_hour": done / (3600 / 3600), # 按实际仿真时长换算 "avg_completion_s": duration_sum / max(done, 1), "success_rate": done / max(total, 1) }

6.2 可视化回放与回归基线

可视化的价值不在好看,在于把死锁过程“放慢”给人看。matplotlib 的 FuncAnimation 足够做栅格地图回放,把日志里的坐标按时间戳逐帧画出来。回放时重点盯两类事件:task_failed之前几秒的车辆分布,以及多车同时规划时的路径重叠。我见过不少算法问题,靠盯回放一眼就看明白了。

回归基线是防止改一处坏全局的保险。做法是:用固定种子固定任务序列,跑一次存下三项指标作为基线;每次改算法或调参,同一任务序列重跑,指标只要比基线差,就说明改动有问题。这条习惯救过我很多次——调度算法的 bug 往往是性能下降而不是崩溃,没有基线根本发现不了。现在我的习惯是:任何 AGV 仿真项目的第一个 commit,先提交基线和复现脚本,再谈改进。仿真平台这东西,跑通只是起点,可信、可复现、可对比才是它真正的价值。希望这份拆解帮你在自己的项目里少走几步弯路,把这套源码真正用起来。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询