☰
Repast多智能体建模实战:从SIR模型到Headless批量仿真
2026/10/20 7:11:34 网站建设 项目流程

简介:一份针对 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 与 Schedule

Context 是所有 Agent 的容器。你可以把它理解成一个“世界”,Agent 以及它们之间的属性都挂在这里。Context 有泛型参数,比如Context<Object>表示容器可以放任何对象,但我建议你自己定义实体类型,比如Context<PersonAgent>,这样遍历时不用做一堆强制转换。

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 下面已经有包结构,而且启动配置自带。

工程里至少要有一个类实现ContextBuilder<T>。这个类是整个模型的入口,Simphony 启动时会调用它的 build 方法。注意 build 方法要返回带好 Agent 和 Projection 的 Context,而不是返回 null。如果你在项目中找不到这个类,Simphony 会直接报错。

public class SimpDemoContext implements ContextBuilder<Object> { @Override public Context build(Context<Object> context) { // 创建一个连续空间,范围是 100x100,原点在 (0,0) ContinuousSpace<Object> space = new ContinuousSpace.Builder<Object>( "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 ContinuousSpace<Object> 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 管 Agent,Projection 管空间,Schedule 管时间。三者缺一,模型就散架。

3. 跑通一个实例:SIR 传染病传播模型

这一章我们做一个经典的 SIR 模型。它足够小,却能覆盖空间投影、Agent 状态迁移、数据采集三个核心环节。这个模型在离线报告中经常出现,也是很多课程设计的标配。

3.1 模型设计:SIR 状态机和接触传播

SIR 模型把人群分成 Susceptible(易感)、Infected(感染)、Recovered(康复)三类。每个 tick,感染者以一定概率感染接触到的人,同时以一定概率康复。关键参数是感染率 β 和康复率 γ。如果你用均匀混合模型,一个 tick 内所有感染者都可能碰到所有易感者,这样传播曲线会非常陡峭,但现实中的空间接触往往更局部。所以这里我加入一个网格投影来限制接触范围。

在设计 Agent 时,我用一个枚举表示状态。为了后续好统计,我建议用枚举而不是 int:

public enum State { SUSCEPTIBLE, INFECTED, RECOVERED }

每个 Agent 持有自己的状态,step 时根据状态执行不同逻辑。感染者遍历其空间相邻格子里的 Agent,如果对方是易感者,就以概率 β 感染它;自己再以概率 γ 恢复。这样做的优势是空间结构会自然导致局部爆发,不会像均匀混合模型那样瞬间传遍全局。

这里要特别说明:遍历相邻格子时需要拿到网格投影(Grid)而不是连续空间。ContinuousSpace 适合移动,Grid 适合离散邻域查找。实际模型里可以同时存在两个投影:一个 ContinuousSpace 用来移动,一个 Grid 用来算邻居。Repast 允许一个 Context 挂多个 Projection,Agent 的位置在不同投影里可以独立维护。

3.2 用网格投影让 Agent 发生空间接触

先创建一个 Grid 投影并加入 Context:

Grid<Object> grid = new Grid.Builder<Object>("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。这个方法返回的是一个迭代器,遍历它还算方便。如果网格里还没有这个 Agent,getMooreNeighbors会查不到,所以要先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 的代码配置大概是这样:

DataSetBuilder<Object> dsb = new DataSetBuilder<Object>("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.headless=true,并且把模型代码和可视化代码严格分文件。模型类不要 import 任何repast.simphony.visualization包。这样即使误启动 GUI 依赖,也不会把模型进程带崩。我自己的习惯是:只要代码里出现Chart或Display,就单独放进一个 display 包,永远不跟 Agent 逻辑混在同一个类里。

5. 把仿真做快:脱离 GUI 跑 Headless 模式的进阶技巧

参数扫描的最终归宿是 Headless。脱离 GUI 之后,仿真速度能提升不少,而且可以批量写脚本跑。这里我用一个最简的流程示范,基于上一章的 SIR 模型。

首先,在 scenario.xml 里把需要扫描的参数声明为变量,比如:

<parameter name="beta" type="double" defaultValue="0.3" /> <parameter name="gamma" type="double" defaultValue="0.1" />

然后写一个启动脚本,通过 Java 系统属性传入参数。具体命令因运行环境而异,不过套路一样:

java -Djava.awt.headless=true -Drepast.beta=0.3 -Drepast.gamma=0.1 \ -Drepast.seed=42 -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 跑通,再换随机种子跑三组对比,确认输出稳定后才开始正式做实验。这个习惯帮我避免了很多在论文阶段才发现模型跑不出来的尴尬。希望帮到你。

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

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

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

立即咨询