简介:一套完整的Repast教程与实例包,面向社会科学、经济学及复杂系统研究者,也适合需要上手多Agent仿真建模的工程师。资源包合计七十二个文件,主体为三十八个HTM与二十八个HTML格式的教程页面和JavaDoc接口文档,另含CSS样式、界面示意图及示例代码压缩包,整体体积仅三百零三KB,轻量易用,便于离线查阅。已有七百三十人学习浏览,是Repast入门与提高的参考资源。内容源自英文原版教材,系统覆盖Repast安装与开发环境设置、Agent定义与交互规则、模型空间构建、Jython与Groovy脚本控制、界面可视化、运行调试、数据收集分析等全流程;HowTo系列按主题拆分为三十余个分步讲解,并配套H2R的完整示例模型与代码,方便对照练习,掌握从概念设计到仿真实验落地的实操路径,内容编排由浅入深,适合循序学习。
1. 先别急着装:Repast 完整教程和实例这套资源到底帮你省掉什么
如果你真想跑通 Repast,最折磨人的不是仿真理论,而是“完整教程和实例”四个字背后那堆版本混乱、API 对不上、环境装不起来的现实。我见过某开发者按着一份老教程折腾三天,最后发现代码里全是 uchicago.src 开头的旧包,而自己装的是 Repast Simphony,两套东西根本不互通。这套资源的价值就在于把“该装哪个版本、怎么搭工程、怎么让模型跑出数据”一次性对齐,让你从翻车现场里省出大量时间。适合人群很明确:要交课程设计的学生、需要出仿真图的从业者,以及所有被 Repast 环境劝退过的人。
2. Repast 选型:装错版本等于白学,先分清三套 API
Repast 教程多,但坑也集中在版本上。我拆过几份网上的“完整实例”,发现一半跑不起来的原因根本不是模型逻辑,而是把 Repast 3 的代码硬塞进 Repast Simphony 里跑。选型这一步做不对,后面所有努力都是白费。
2.1 三套版本的分工:GUI 建模、批处理、Python 并行
Repast 家族里你实际会遇到三个主流分支,它们的导入语句、项目结构、运行方式完全不一样:
| 版本 | 主要语言 | 特征 | 适合场景 |
|---|---|---|---|
| Repast 3 | Java / Python | 老牌库,包名以uchicago.src.*开头 | 维护旧代码,入门不推荐 |
| Repast Simphony | Java / Groovy | 基于 scenario 的可视化建模,注解驱动调度 | 绝大多数教程、课程设计、中小规模仿真 |
| Repast4py | Python | 面向高性能计算,支持分布式并行 | 大规模群体仿真、需要 MPI 的场景 |
判断一份实例代码属于哪个版本,不要看标题,直接看 import 语句。repast.simphony.*是 Repast Simphony;uchicago.src.*是 Repast 3;repast4py.*是 Python 版。很多新手栽跟头,就是因为下载实例后没做这一步,直接照着旧代码写新工程,自然过不了编译。
我一般只推荐 Repast Simphony。它有一整套可视化 Scenario 工具,Agent、网格、网络、GIS 投影都能在界面上点选配置,同时保留了纯代码建模的能力,对初学者和做实验的人来说最平衡:既不像 Repast 3 那样原始,又不像 Repast4py 那样需要并行计算基础。
2.2 核心建模抽象:Context、Projection、Agent、Schedule 各自管什么
Repast Simphony 的建模思路和普通 Java 类库不一样,它是一套完整的“模型容器”框架。第一次接触的人容易把这几个概念混在一起,实际分工很清楚:
- Context:Agent 的容器,负责管理对象生命周期。所有 Agent 必须
add进 Context 才会被调度器感知。 - Projection:Agent 在空间里的“位置关系”,常见有网格 Grid、连续空间 ContinuousSpace、网络 Network、GIS 投影。一个 Agent 可以同时加入多个投影,比如“在网格上移动,在网络上传消息”。
- Agent:普通 Java 对象(POJO),不需要继承任何基类。这是 Repast 的设计特点,也经常被初学者忽略——到处找 Agent 基类,其实根本不存在。
- Schedule:事件调度器。它扫描 Agent 中加了
@ScheduledMethod注解的无参 public 方法,按 tick 周期调用。
从创建到运行的完整链路是这样:初始化模型时先建 Context,再建 Projection 并挂到 Context,然后把 Agent 逐个 add 进去并放到投影位置上,最后启动调度器。之后每个 tick,调度器就会按注解规则调用 Agent 的方法。理解这条链路,“为什么我的 Agent 不动”这类问题就解决了一半。
2.3 教程资源包里一般有什么,怎么把文档和代码对齐
一份合格的 Repast 完整教程和实例资源,通常包含这样几块内容:
| 内容 | 作用 |
|---|---|
| 入门教程文档 | 环境搭建、版本选型、第一个模型的完整步骤 |
| 可运行 Demo | SIR 传染病、库存系统、排队模型等经典案例 |
| Scenario 文件 | 每个 Demo 配套的.rs场景和model.score定义 |
| README | 各 Demo 的 JDK 版本、运行方式、参数说明 |
拿到这类资源后,我的习惯是“先跑通最小 Demo,再看教程”。直接从头读文档容易晕,先随便挑一个最简单的模型跑起来,对工程结构有了体感,再回头读概念效率高得多。一次跑通后,再对着 README 做三件事:确认版本号、确认 scenario 文件路径是否完整、确认随机种子是否写在参数里。做完这三步,实例才能稳定复现,否则后面批量实验时会吃大亏。
3. 把第一个模型跑起来:从空工程到 100 个 Agent 在网格上移动
很多 Repast 教程上来就让你装 Eclipse 插件,然后在可视化界面里点点点,最后工程文件一团糟,连代码放哪都找不到。我更推荐另一种路线:用 Gradle 建工程,代码定义模型结构,Scenario 只做运行入口。这样结构清晰,也方便备份和对比。
3.1 工程骨架:用 Gradle 而不是 Eclipse 插件
Eclipse 插件方式最大的问题是版本耦合:插件版本、Eclipse 版本、JDK 版本任何一个不匹配,插件菜单就消失,属于典型的“环境玄学”。用 Gradle 方式可以把依赖和运行方式写进脚本,换机器也能快速重建。
plugins { id 'java' // Repast Simphony 的 Gradle 插件,版本号与本地 JDK 绑定,不要盲目追新 id 'repast.simphony' } repositories { mavenCentral() } dependencies { // 具体坐标版本按你手里教程资源包的说明填,我这里用占位符 implementation 'repast.simphony:repast-simphony:x.y.z' } tasks.register('runModel', JavaExec) { main = 'repast.simphony.runtime.RepastMain' args = ['scenario.rs'] }这个脚本的核心是两个关键点:一是repast.simphony插件负责把 Repast 的运行时和模型编译环境接好;二是runModel任务指定了入口类repast.simphony.runtime.RepastMain,参数是 scenario 文件路径。为什么用scenario.rs而不是直接写一个main方法?因为 Repast Simphony 的运行单位是 scenario,模型结构、参数定义、输出配置都挂在.rs文件上,代码只是其中的行为部分。这种设计的好处是:改参数、换投影不用动 Java 代码,改.rs就行。
3.2 定义 Agent:注解驱动的方法调度
有了工程骨架,接下来定义最简单的 Agent。Repast Simphony 的 Agent 不需要继承任何抽象类,只需要写一个普通类,把要周期执行的方法标上注解。
package demo; import repast.simphony.annotations.ScheduledMethod; import repast.simphony.random.RandomHelper; public class Worker { private int x; private int y; private final int stepSize = 1; public Worker(int x, int y) { this.x = x; this.y = y; } @ScheduledMethod(start = 1, interval = 1) public void step() { // 每次 tick,x 和 y 在 [-1, 1] 范围内随机移动 x += RandomHelper.nextInt(3) - 1; y += RandomHelper.nextInt(3) - 1; } public int getX() { return x; } public int getY() { return y; } }这段代码里,@ScheduledMethod(start = 1, interval = 1)的含义是:从第 1 个 tick 开始,每 1 个 tick 调用一次step()。方法必须是public void且无参数,否则调度器注册时会报错或直接跳过。RandomHelper是 Repast 封装的随机数工具,它和后面要讲的随机种子绑定,比直接new Random()更可控。
这里有一个很多新手都踩过的点:注解写了,方法也写了,但 Agent 没加入 Context,结果模型跑起来后这个类根本没被调度。Agent 和调度器之间不是靠类扫描,而是靠 Context 注册。这个坑后面第 5 章还会展开讲。
3.3 网格投影与示例模型:把 100 个 Agent 放进二维网格
Agent 定义好之后,需要一个模型初始化类来创建 Context、网格和 Agent 实例。
package demo; import repast.simphony.context.DefaultContext; import repast.simphony.space.grid.Grid; import repast.simphony.space.grid.GridBuilder; import repast.simphony.random.RandomHelper; public class DemoContext extends DefaultContext<Object> { public DemoContext() { super("demo-context"); // 创建 50 x 50 的网格投影,8 邻域连通 GridBuilder<Object> builder = new GridBuilder<>("mainGrid"); builder.size(50, 50); builder.adjacency(8); Grid<Object> grid = builder.build(); addProjection(grid); // 生成 100 个 Worker,并放到随机格点上 for (int i = 0; i < 100; i++) { int x = RandomHelper.nextInt(50); int y = RandomHelper.nextInt(50); Worker worker = new Worker(x, y); add(worker); grid.moveTo(worker, x, y); } } }代码里有三个关键顺序不能乱:先addProjection(grid)把网格挂进 Context,再add(worker)让调度器认识这个 Agent,最后grid.moveTo(worker, x, y)把 Agent 放到具体坐标。如果只 add 不 moveTo,Agent 虽然存在但不在任何网格点上,后续查邻居、算距离都会得到空结果。adjacency(8)表示 8 邻域(上下左右加四个对角方向),如果模型逻辑按 4 方向移动就填 4,这个参数直接影响邻居计算范围。
在 scenario 里,需要把这个DemoContext绑定为模型的上下文类。通常的做法是右键 scenario 里的 Context 节点,类型选择 Java class,然后填demo.DemoContext。绑定完成后,每次改完 Java 代码,都要重新加载一下 scenario,否则跑的还是旧编译结果。
3.4 运行与确认:日志输出和时序监控
运行模型有两种常见方式:在界面里直接点 Run(可视化模式),或用命令行 headless 方式跑。第一次调试推荐 GUI 模式,因为它能看到 Grid 显示,Agent 动没动一目了然。确认模型正常后,再切到 headless 做批量。
无论哪种模式,我都建议在 Agent 里加一行日志,确认方法确实在被调度:
import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class Worker { private static final Logger logger = LogManager.getLogger(Worker.class); @ScheduledMethod(start = 1, interval = 1) public void step() { x += RandomHelper.nextInt(3) - 1; y += RandomHelper.nextInt(3) - 1; // 只在调试期打开,批量实验时改成 info 级别避免刷屏 logger.debug("tick move to ({}, {})", x, y); } }日志级别建议用 debug,GUI 调试时打开,批量实验时改成 info 或关闭,避免几千个 Agent 刷出几十万行日志。看到tick move to的输出,说明从 Context 注册到调度执行整条链路都通了,接下来才能放心做参数实验。
4. 调度器与参数扫描:让模型按你的节奏走
模型能跑起来只是第一步,真正决定仿真质量的是调度节奏和参数控制。Repast 的调度器对很多人来说是个黑匣子:不知道方法按什么顺序执行,不知道 interval 和真实时间的关系,更不知道批量实验时数据从哪出来。这一章把这些点全部拆开。
4.1 调度机制:start、interval、priority 怎么用
调度器按“离散 tick”推进,tick 不代表秒或毫秒,只代表事件顺序。同一 tick 内如果有多个方法要执行,就需要 priority 决定先后。
@ScheduledMethod(start = 1, interval = 1, priority = 10) public void move() { // 先移动:priority 大,先执行 } @ScheduledMethod(start = 1, interval = 1, priority = 0) public void infect() { // 移动完成后,再处理传染 } @ScheduledMethod(start = 1, interval = 1, priority = -10) public void record() { // 最后统计和记录数据 }priority 数值越大越先执行。上面这个顺序就是典型的“移动 → 交互 → 统计”三段式,和传染病模型里“先走路,再接触传染,最后记录病例数”的逻辑完全吻合。如果不设 priority,同 tick 内多个方法的执行顺序在逻辑上是不确定的,模型稍微复杂一点就可能出现“先统计后传染”的诡异结果。
interval的另一个隐含行为是:调度器会自动对齐不同周期的方法。比如一个方法每 2 tick 执行一次,另一个每 3 tick 执行一次,那么它们每 6 tick 会同时触发一次。这个特性常用于生成报表类任务:Agent 每天更新状态,报表每 7 天汇总一次,两者不会冲突。
4.2 批量实验:Parameter Sweeps 配置与随机种子
做完单次运行,下一步就是参数扫描。Repast Simphony 里,参数定义在 scenario 的 Parameters 面板中,运行时通过 setter 注入到模型类。常见的库存模型参数表如下:
| 参数名 | 类型 | 典型范围 | 默认值 |
|---|---|---|---|
| seed | long | 固定值或随机 | 42 |
| reorderPoint | int | 10 - 30,步长 5 | 20 |
| demandMean | double | 2.0 - 8.0,步长 1.0 | 5.0 |
| refillQty | int | 30 / 50 / 100 | 50 |
在批量运行对话框里选择 Parameter Sweeps,把 reorderPoint 设为 10 到 30 步长 5,把 refillQty 设为 30、50、100,Repast 会自动组合出多组参数并逐个运行。这里最重要的一个参数是 seed。把 seed 设成固定值,整批实验结果才可复现;seed 随机的话,同一组参数跑两次结果不同,论文里根本没法解释。
模型类接收参数的方式建议用 setter:参数面板里的 reorderPoint 会自动调用模型里对应的setReorderPoint(int value)方法。这样代码层面不需要引入额外的注解,模型和实验配置完全解耦。批量跑完后,每个参数组合会输出一份独立的结果文件,文件命名里建议带上 run 号,否则上次的结果会被覆盖。
4.3 数据落地:FileSink 输出 CSV,记录每个 tick
模型跑完要有数据,否则和没跑一样。Repast 最常用的输出组件是 FileSink,它的配置方式如下:
| 配置项 | 建议值 | 说明 |
|---|---|---|
| 输出文件 | output/data_${run}.csv | 用 ${run} 区分不同参数组,避免覆盖 |
| 分隔符 | 逗号 | 标准 CSV,方便 Python/R 读取 |
| 数据源 | Worker.x, Worker.y | 从 Agent 对象取属性或方法返回值 |
| 输出周期 | 每个 tick | 在调度器的统计方法里触发 record |
配置数据源时要注意:FileSink 只能读取对象公开的属性或者 getter 方法。比如 Worker 类里有getX(),数据源才能填Worker.x。没有 getter 的私有字段,FileSink 是读不到的。输出周期通常绑在一个专门的统计方法上,用priority = -10保证它最后执行。这样每 tick 结束时,所有 Agent 的当前位置和全局统计量都沉淀到 CSV 里。
5. Repast 常见问题与排查:五个浪费过我半天时间的坑
下面这五条都是我在拆教程实例时真实遇到过的,每条按“现象 → 原因 → 解决”的顺序写,你可以直接对照排查。
5.1 Eclipse 插件菜单消失,向导里找不到 Repast
现象:按教程装完插件,重启 Eclipse 后菜单里什么都没有,新建项目向导里也搜不到 Repast。
原因:绝大多数情况是 JDK 版本、Eclipse 版本和插件版本三者不匹配。Repast Simphony 插件对 Eclipse 版本很敏感,很多教程只写了“安装插件”,没写清楚验证过的版本组合。还有一个容易被忽略的原因是安装路径带中文或空格,插件加载失败。
解决:不再依赖 Eclipse 插件,改用 3.1 的 Gradle 方式创建工程。如果一定要用插件,按教程资源包里标注的 JDK 和 Eclipse 版本组合重新装一遍,不要用最新版。装完先在“安装详情”里确认插件已注册,再新建项目。
5.2 模型跑起来了,但 @ScheduledMethod 方法从未执行
现象:GUI 里网格显示正常,Agent 也出现在画面上,但它们的坐标永远不变,日志里没有任何输出。
原因:Agent 没有加入 Context。调度器只扫描 Context 里的对象,你new出来的对象如果不add进去,注解写得再对也不会被调用。另一个常见原因是注解导入错了包,把repast.simphony.annotations.ScheduledMethod和别的注解类混用。
解决:回到初始化代码,确认每个 Agent 创建后都调用了context.add(agent)。同时检查方法签名:必须是public void且无参数。可以在 step() 第一行临时加一个System.out.println验证,跑通后再换成 logger。
5.3 同样的参数,两次运行结果完全不一样
现象:固定了所有参数,重新运行模型,输出曲线的形状和数值对不上,甚至差别很大。
原因:随机种子没有固定,或者代码里混用了RandomHelper和new Random()。Repast 的随机数体系是围绕种子统一管理的,你在代码里绕开它,等价于每次跑都在用不同的随机序列。另外,如果代码里用 HashSet 遍历 Agent,迭代顺序不稳定,也会导致结果漂移。
解决:把 seed 作为显式参数写进 Parameters 面板,模型启动时调用RandomHelper.setSeed(seed)。集合遍历统一用 ArrayList 或按 Agent 的稳定标识排序,不要依赖哈希顺序。改完之后跑两遍相同配置,结果应该逐字节一致。
5.4 headless 批量模式报错:找不到某 GUI 类
现象:GUI 模式运行正常,一换到 headless 批量模式就抛异常,提示某个图表类或显示组件无法实例化。
原因:初始化代码里直接 import 并创建了 Chart、Display 等可视化对象。GUI 模式有显示环境,能正常加载;headless 模式没有显示环境,这些类一实例化就炸。
解决:把 GUI 构建逻辑单独抽到一个类里,只在交互式运行时调用。模型核心代码只做逻辑和数据输出,不依赖任何显示组件。这样同一套模型既能 GUI 调试,又能 headless 批量跑。
5.5 CSV 输出只有表头,没有数据行
现象:FileSink 配置完,运行模型,打开 CSV 发现只有列名,底下是空的。
原因:数据源表达式配置错误,最常见的是对象名或属性名拼写问题,FileSink 匹配不到数据,就直接跳过写入。另一个原因是输出周期没触发——record 方法没被调度器调用,或者调用的时间早于数据生成。
解决:先在 FileSink 配置界面里确认数据源下拉框能列出目标对象的属性和 getter,然后检查 record 方法上有没有@ScheduledMethod注解。我习惯用一个priority = -10的统计方法统一做 record,确保它在本 tick 所有业务逻辑之后执行。
6. 进阶技巧:无头模式批量跑完实验,再用 Python 画一张能放进论文的图
6.1 无头模式批量跑
参数扫描在界面上点起来很快,但一旦参数组数量超过几十组,就应该切到无头模式。用下面的 Gradle 任务可以直接在终端启动:
./gradlew runModel -PrunScenario=scenario.rs输出文件统一落在output/目录,文件名里带上 run 号。跑完后先看一眼每个 CSV 的行数和表头,确认没有出现第 5 章里说的“空数据”问题,再做可视化。
6.2 用 Python 读取 CSV 画对比图
Repast 输出的 CSV 天生就是给数据分析工具用的。我用 Python 读取多组参数的结果,画在同一张图里对比:
import pandas as pd import matplotlib.pyplot as plt files = [ "output/data_1.csv", # reorderPoint=10 "output/data_2.csv", # reorderPoint=20 "output/data_3.csv", # reorderPoint=30 ] for f in files: df = pd.read_csv(f) # 假设 CSV 里有 tick 和 stock 两列 plt.plot(df["tick"], df["stock"], label=f) plt.xlabel("tick") plt.ylabel("stock") plt.legend() plt.grid(True) plt.savefig("stock_comparison.png", dpi=150)画图时我一般会把同一参数组的多条随机种子曲线叠加,取均值和标准差,画成带阴影的区间图,这样比单条曲线更有说服力。因为固定了 seed,每一组都能稳定复现;如果发现曲线在某个参数区间出现剧烈跳变,再回头细扫那个区间的参数,定位敏感点。
从那以后,我每接到一个新 Repast 模型,第一件事就是先固定 seed、跑 100 个 tick、抓一份 CSV、把日志窗口关掉,确认整条链路稳定,再开始调参数。这套流程帮我避开了多数环境问题和数据问题,希望也能帮到你。
本文还有配套的精品资源,点击获取