
简介一份针对 Repast 建模与仿真平台的英文原版教程与配套实例面向社会科学、经济学及复杂系统领域的研究者也适合希望快速上手多智能体仿真的学生和工程师。整个资源包内共包含72个文件主体为66个HTML/HTM格式的教程页面浏览器打开即可直接阅读另有少量CSS样式、示例图片与代码压缩包整体大小仅303KB轻量便于查阅。教程内容系统覆盖安装设置、核心概念、模型构建、脚本控制、可视化界面、运行调试及数据收集分析并通过H2R等案例演示从概念到实现的完整流程。随附的文档与代码可对照练习能帮助读者理解Repast的Agent创建、交互规则设定和并行仿真特性。已有730人下载学习对于零基础入门或需系统梳理Repast方法的使用者是一份实用且高效的参考资料。1. Repast 仿真框架为什么我还在用它做多智能体建模做过多智能体仿真的人基本都被问过为什么不用 NetLogo我通常的回答是当你需要真正的 Java 代码来控制 Agent 行为、要跑上万次参数扫描、要对接外部数据时NetLogo 会显得轻薄而 Repast 的扩展性刚好接得住。这套 repast 完整教程和实例我拆过一遍能解决从零安装到跑通一个 SIR 模型到 headless 批量仿真的所有问题。它适合有 Java 基础的研究生和做地理模拟的工程师不适合只想拖拽点几下就出结果的速成派。下面我会把建模、调度、参数扫描、坑位一个一个过。2. 建一个最小模型从 Agent 定义到调度器配置在 Repast 生态里Simphony 是当前最值得投入的那个分支。它的核心不是界面而是几个 Java 接口组合出来的模型骨架。这一章从原理讲起再落到工程代码保证你看完能自己搭出第一个能跑的模型。2.1 认识 Repast Simphony 的三个核心概念Context、Projection 与 ScheduleContext 是所有 Agent 的容器。你可以把它理解成一个“世界”Agent 以及它们之间的属性都挂在这里。Context 有泛型参数比如ContextObject表示容器可以放任何对象但我建议你自己定义实体类型比如ContextPersonAgent这样遍历时不用做一堆强制转换。Projection 是空间组织方式。常见的有ContinuousSpace连续空间和Grid网格。Projection 本身不持有 Agent它只是从 Context 中读取 Agent 的位置。所以向 Context add 一个 Agent 之后必须再把它插入到 Projection 里否则空间查找会找不到它。很多人一开始就在这里翻车把 Agent add 进 Context 就以为有了坐标结果 getNeighbors 返回空。Schedule 是全局时钟。它决定每个 Agent 在哪个 tick 执行哪个方法。Repast 的调度器不是简单地按固定频率循环它支持同时执行、优先级、延迟执行甚至随机的 tick 间隔。理解这三者之后再看代码就不会迷路。很多嵌套模型跑出来的现象诡异多半是这三个角色的职责没有分清楚。2.2 安装与工程结构不只是建个 Maven 项目常见做法是在 IDE 里安装 Repast Simphony 插件然后新建一个 RePast Simphony project。这个插件会帮你生成一个 lib/ 目录里面放着 repast.simphony 的 jar 包。如果你不想依赖插件也可以手动建 Maven 项目把 repast.simphony 依赖加进去但插件会顺手把 scenario.xml 和默认 run 配置都搭好省掉不少琐碎事。我一般建议用插件建工程因为默认的 src/main/java 下面已经有包结构而且启动配置自带。工程里至少要有一个类实现ContextBuilderT。这个类是整个模型的入口Simphony 启动时会调用它的 build 方法。注意 build 方法要返回带好 Agent 和 Projection 的 Context而不是返回 null。如果你在项目中找不到这个类Simphony 会直接报错。public class SimpDemoContext implements ContextBuilderObject { Override public Context build(ContextObject context) { // 创建一个连续空间范围是 100x100原点在 (0,0) ContinuousSpaceObject space new ContinuousSpace.BuilderObject( space, new Point(100, 100), new Point(0, 0)) .build(); context.addProjection(space); // 创建 10 个 Agent 并放到随机位置 for (int i 0; i 10; i) { PersonAgent p new PersonAgent(i); context.add(p); space.moveTo(p, randomPoint()); } return context; } private Point randomPoint() { Random r new Random(); return new Point(r.nextDouble() * 100, r.nextDouble() * 100); } }这段代码里ContinuousSpace.Builder需要三个参数投影名、范围上界Point(100,100)、范围下界Point(0,0)。范围下界不传的话默认是原点。注意moveTo方法的第二个参数必须是Point对象不是坐标数组。我见过有人手滑传成(x, y)结果编译报错报了半天。build方法是模型入口在每次仿真启动时被调用。如果这个类没有在配置里指定Simphony 会报“找不到 context builder”的错误。配置一般在 scenario.xml 里或者直接用注解ContextBuilder指定。注解更直观但多人协作时我还是用 scenario.xml 指定因为不用改代码就能切模型入口。2.3 写一个会移动的 Agent代码里到底在继承什么Repast Simphony 不强求 Agent 继承某个基类它更像一个接口规范。你只需要给类加上Agent注解并且让方法接受到调度。下面这个PersonAgent是最小可运行版Agent public class PersonAgent { private int id; private ContinuousSpaceObject space; private boolean state; public PersonAgent(int id) { this.id id; } ScheduledMethod(start 1, interval 1, priority 1) public void step() { Random r new Random(); double dx (r.nextDouble() - 0.5) * 2; double dy (r.nextDouble() - 0.5) * 2; Point newPt space.displace(this, new Point(dx, dy)); space.moveTo(this, newPt); } }这里ScheduledMethod的start表示从第 1 个 tick 开始执行interval表示每个 tick 都执行priority是调度优先级数值越大越先执行。state变量可以表示健康状态在这个最小模型里它只是一块占位后续扩展 SIR 模型时会派上用场。注意step方法里我并没有把 Agent 从 Context/Projection 里移除只是修改它在空间中的位置。space.displace会根据当前坐标加上位移向量返回一个新点。之后moveTo把它移动过去。如果不小心用了space.moveByDisplacement效果类似但参数是位移向量容易混淆。给 Agent 注入空间的方式最好是在 build 方法里通过 setter 传进去不要依赖注解自动装配。注解装配能省代码但在复杂模型里会让人抓狂因为你看不见对象间的依赖关系。2.4 把模型跑起来配置 Schedule 与主类上面代码只是定义了 Agent还需要一个“踢一脚”的入口。Repast 的模型入口是在某个类的ScheduledMethod里不断触发全局 step或者在 ContextBuilder 里直接启动。实际上 Simphony 的默认流程是先调用 ContextBuilder.build 填充环境然后运行内置的 DefaultSchedule。你不需要写 main 方法Simphony 的运行时会在启动模型时自动拉起一个线程。但如果你要脱离 IDE 做自动化就要自己在 main 里启动。代码如下public class RunModel { public static void main(String[] args) { RepastSimphony sim new RepastSimphony(); sim.setContextBuilder(new SimpDemoContext()); sim.run(); } }RepastSimphony是运行时控制类。run方法会阻塞当前线程直到仿真结束。如果要跑固定 tick 数可以设置 run 参数或者从 Schedule 外部拿一个 TimeDuration。实际调试时我更推荐直接在 scenario 的配置里设置“自动暂停于 N tick”这样可以边跑边看图形不用等跑完。这一章的核心信息是Context 管 AgentProjection 管空间Schedule 管时间。三者缺一模型就散架。3. 跑通一个实例SIR 传染病传播模型这一章我们做一个经典的 SIR 模型。它足够小却能覆盖空间投影、Agent 状态迁移、数据采集三个核心环节。这个模型在离线报告中经常出现也是很多课程设计的标配。3.1 模型设计SIR 状态机和接触传播SIR 模型把人群分成 Susceptible易感、Infected感染、Recovered康复三类。每个 tick感染者以一定概率感染接触到的人同时以一定概率康复。关键参数是感染率 β 和康复率 γ。如果你用均匀混合模型一个 tick 内所有感染者都可能碰到所有易感者这样传播曲线会非常陡峭但现实中的空间接触往往更局部。所以这里我加入一个网格投影来限制接触范围。在设计 Agent 时我用一个枚举表示状态。为了后续好统计我建议用枚举而不是 intpublic enum State { SUSCEPTIBLE, INFECTED, RECOVERED }每个 Agent 持有自己的状态step 时根据状态执行不同逻辑。感染者遍历其空间相邻格子里的 Agent如果对方是易感者就以概率 β 感染它自己再以概率 γ 恢复。这样做的优势是空间结构会自然导致局部爆发不会像均匀混合模型那样瞬间传遍全局。这里要特别说明遍历相邻格子时需要拿到网格投影Grid而不是连续空间。ContinuousSpace 适合移动Grid 适合离散邻域查找。实际模型里可以同时存在两个投影一个 ContinuousSpace 用来移动一个 Grid 用来算邻居。Repast 允许一个 Context 挂多个 ProjectionAgent 的位置在不同投影里可以独立维护。3.2 用网格投影让 Agent 发生空间接触先创建一个 Grid 投影并加入 ContextGridObject grid new Grid.BuilderObject(diseaseGrid, new int[] {100, 100}) .build(); context.addProjection(grid);Grid.Builder的第二个参数是维度数组必须用int[]不能用两个 int。之后 Agent 在移动时每 tick 把它的网格坐标更新到 grid 上。最简单的方法是让 Agent 在移动后调用grid.moveTo(agent, x, y)。注意这里 x 和 y 是整型坐标如果你从连续空间直接拿 double 坐标要先取整。邻域查找用grid.getMooreNeighbors(agent, 1)能拿到半径 1 的八邻域内的所有 Agent。这个方法返回的是一个迭代器遍历它还算方便。如果网格里还没有这个 AgentgetMooreNeighbors会查不到所以要先moveTo再查邻居。ScheduledMethod(start 1, interval 1, priority 1) public void step() { move(); if (state State.INFECTED) { infectNeighbors(); if (random.nextDouble() recoveryRate) { state State.RECOVERED; } } }我把 move 和感染拆开写了两个方法便于复用。感染逻辑里随机数统一用模型级别的一个 Random 实例不要每个 Agent 自己new Random()否则多线程环境下种子行为会非常难调。这块也是第 4 章要重点说的坑。这里的infectNeighbors方法里会调用grid.getMooreNeighbors所以 step 的顺序必须是先更新 grid 坐标再执行感染逻辑。3.3 数据采集从 Simphony 的 Data Recorder 到自定义输出Simphony 自带 DataRecorder 可以用来定时记录 Agent 属性。它的配置在 scenario.xml 里需要指定一个 DataSetBuilder 来监控某个 Agent 集合。我试过在代码里直接调 DataRecorder但感觉更灵活的做法还是自己写一个监视器 Agent它每个 tick 统计一次全局状态写到 CSV。这样不用和 GUI 的刷新机制耦合。Agent public class DataMonitor { ScheduledMethod(start 1, interval 1) public void record() { int s 0, i 0, r 0; for (Object obj : context.getAgentList()) { PersonAgent p (PersonAgent) obj; switch (p.getState()) { case SUSCEPTIBLE: s; break; case INFECTED: i; break; case RECOVERED: r; break; } } System.out.println(tick , s , i , r); } }上面这段直接 print 到控制台配合重定向就能当 CSV 用。正式做实验时应该用 BufferedWriter 写入文件并且每次以追加模式打开。tick 的获取可以用repast.simphony.engine.environment.RunEnvironment提供的时钟接口也可以通过ScheduledMethod的方法参数注入但需要在调度器里额外配置。这个监视器 Agent 不必须继承任何基类只要在 ContextBuilder 中 add 进 Context 即可。它没有空间位置所以不需要插入到 Projection 里。这种“无实体 Agent”很适合做各类统计。你看Repast 的灵活性这时候就体现出来了。3.4 运行与可视化图表里看到的传播曲线运行模型后Simphony 的 Scenario Tree 会显示可用的投影与图表。你可以新建一个 Time Series Chart选择 DataMonitor 的输出流。常见的坑是图表只能显示 Simphony 自己的 DataSet如果你用控制台输出图表不会自动感知。所以为了在 IDE 里看曲线还是需要配置 DataSetBuilder。DataSetBuilder 的代码配置大概是这样DataSetBuilderObject dsb new DataSetBuilderObject(SIRData); dsb.addCount(susceptible) .addMethod(infected, new MethodCaller(...));这里的 MethodCaller 参数比较复杂我一般在监控 Agent 内部维护一个 Map把三个状态数量写入DataSetBuilder 再从中读。这样可以把数据采集和展示解耦不然一不小心就会掉进反射调用的坑。反射调用如果方法签名对不上运行时报错的时候你都不知道是谁调的。可视化只是辅助最终结论还是看导出的数据。4. 参数扫描与结果导出批量跑仿真避坑记录跑单次模型谁都会真正麻烦的是参数扫描。这一章把我踩过的坑集中整理每一条都是“现象 → 原因 → 解决”的结构希望能帮你少走弯路。4.1 坑一用 GUI 跑参数扫描卡死现象在 Simphony 界面里拖了十多个参数组合点 run 之后整个界面假死鼠标转圈风扇狂转。原因GUI 模式下每个参数组合都会尝试创建一个可视化窗口。尤其是网格投影和历史折线图会占用大量内存并且刷新线程互相抢锁。参数组合一多多线程刷新叠加界面自然卡死。解决我后来很少在 GUI 里做批量扫描。优先用 Headless 模式也就是下一章说的无界面运行。如果一定要 GUI减少参数组合数量并且把图表关闭。右键点击 Chart 组件把它 remove 掉只保留控制台输出。一次跑 5 个组合可以超过 50 个就不要在 GUI 里等了。4.2 坑二多个随机种子下结果无法复现现象用同一组参数、同一个种子跑两次最后输出的 CSV 曲线却不一样。原因代码里有多处new Random()它们各自独立消耗随机状态的顺序不同。尤其是 Agent 移动时如果同时用了全局随机和局部随机种子的走位完全不可控。解决所有随机数走同一个 Random 实例在 ContextBuilder 里创建注入给每个 Agent。如果需要多线程使用 ThreadLocal 的随机源并固定初始种子。复现的底线是参数配置里固定随机种子并且代码里不能有依赖哈希迭代顺序的分支。比如遍历一个 HashMap 再生成随机数顺序一变结果就变。4.3 坑三Data Recorder 输出的 CSV 列错位现象CSV 里第一列 tick后面三列分别是数据但某一列出现 NaN 或缺失。原因Data Recorder 的刷新频率比仿真暂停点慢或者记录方法抛异常被吞掉了。更多时候是你在代码里改变了 Agent 的某个属性但没有调用数据源的数据更新通知。解决把数据源改成每次 tick 主动绑定的自定义 DataMonitor输出前做时间戳对齐。不要依赖 Simphony 自动刷新它更适合“查看”不适合做最终实验数据。我一般会让 DataMonitor 直接持有 StringBuffer整行拼好再写入文件这样至少能保证每行数据是完整对齐的。4.4 坑四内存溢出与 Agent 积压现象跑 5000 个 Agent 时前 100 tick 还正常到 300 tick 突然堆内存溢出。原因模型里创建了大量临时对象又没有及时释放比如每个 tick 都new Point和new Random。再加上 Grid 投影底层会保留 Agent 的历史移动轨迹。这里的“保留轨迹”不是指数据流而是某些版本的投影内部会维护一个位置映射表如果你没有把离场 Agent 从投影里移除对象就一直被引用。解决Point 是不可变对象每次位移都 new 是难免的但可以复用随机实例。另外检查代码里是否有正则表达式、字符串拼接产生的临时对象。把 JVM 堆调大只是缓兵之计真正的解法是减少 Agent 生命周期内的对象创建。对于确实不再需要的 Agent调用context.remove(agent)和grid.remove(agent)别留着让它排队等 GC。4.5 坑五Headless 模式下图形依赖报错现象切到 Headless 跑批量启动没多久报 javafx/awt 相关异常甚至直接崩 JVM。原因有些代码隐式依赖了 GUI 组件。最常见的是你在 Agent 构造时调用了某个需要 java.desktop 模块的方法或者直接用了 Simphony 的图表类。Headless 模式没有显示设备这些方法一执行就抛异常。解决Headless 启动时加 JVM 参数-Djava.awt.headlesstrue并且把模型代码和可视化代码严格分文件。模型类不要 import 任何repast.simphony.visualization包。这样即使误启动 GUI 依赖也不会把模型进程带崩。我自己的习惯是只要代码里出现Chart或Display就单独放进一个 display 包永远不跟 Agent 逻辑混在同一个类里。5. 把仿真做快脱离 GUI 跑 Headless 模式的进阶技巧参数扫描的最终归宿是 Headless。脱离 GUI 之后仿真速度能提升不少而且可以批量写脚本跑。这里我用一个最简的流程示范基于上一章的 SIR 模型。首先在 scenario.xml 里把需要扫描的参数声明为变量比如parameter namebeta typedouble defaultValue0.3 / parameter namegamma typedouble defaultValue0.1 /然后写一个启动脚本通过 Java 系统属性传入参数。具体命令因运行环境而异不过套路一样java -Djava.awt.headlesstrue -Drepast.beta0.3 -Drepast.gamma0.1 \ -Drepast.seed42 -jar your-sim.jar在模型代码里从系统属性读取参数String betaStr System.getProperty(repast.beta, 0.3); double beta Double.parseDouble(betaStr);这样每个参数组合只需要换一次 JVM 参数不涉及 GUI 渲染和鼠标操作跑完再把 CSV 汇聚起来就行了。比在界面里手动拖参数条靠谱得多。验证方法也很简单跑 5 次同一参数对比 CSV 最后一行是否一致。一致说明随机种子可控不一致就回去查哪里没有用同一个 Random 实例。从那以后我每次搭好一个模型都强制把完整流程走一遍先 GUI 跑通再 headless 跑通再换随机种子跑三组对比确认输出稳定后才开始正式做实验。这个习惯帮我避免了很多在论文阶段才发现模型跑不出来的尴尬。希望帮到你。本文还有配套的精品资源点击获取