3.5 KiB
仿真层设计
English | 简体中文
本文件记录仿真层(sim)的跨 crate 决策。各 crate 的内部设计见其模块文档。
组成与依赖
apsis-event 事件类型与事件队列
apsis-sim-engine 离散事件引擎
apsis-scenario 场景定义与文本加载
apsis-output 仿真输出记录
apsis-sim 仿真门面(整合上述四个)
依赖方向:
apsis-event -> apsis-sim-engine -> apsis-sim -> apsis-rs
apsis-scenario -----------------> apsis-sim
apsis-output -------------------> apsis-sim
apsis-sim 同时依赖 ops 层(apsis-scheduler、apsis-schedule、
apsis-task、apsis-resource),因为它要规划场景并读取计划。其余 crate
只依赖 foundation。
事件顺序
同一时刻的多个事件必须按可复现的顺序投递,否则同一输入会产生不同输出, 无法与参考实现对拍。约定:
- 每个事件在入队时获得一个递增的序号。
- 事件先按时间排序,时间相同则按序号排序,即入队顺序。
- 处理函数在处理某事件时新调度的事件,排在该时刻已有事件之后。
该顺序由 apsis-event 保证,apsis-sim-engine 与 apsis-sim 直接沿用。
序列号由队列分配,调用方无法制造两个同号事件。
时间原点
事件时间是从仿真原点起算的秒数,与日历无关;需要日历的场景由上层换算。
场景的 epoch 是时钟起点。时间线中的事件时间相对场景原点,apsis-sim
在投递前加上 epoch。若不偏移,引擎会在投递首个事件时把时钟回拨。
时间限制按同一绝对时间轴比较:时间晚于上限的事件留在队列中而不执行,因此 调用方可以查看运行结束时还剩什么。时钟停在最后一个已执行事件上,而不是停在 上限上。
事件限制是累计的
apsis-sim-engine 的事件上限是引擎整个生命周期的总数,而非单次 run
的数量。达到上限后再次 run 不执行任何事件。这样"上限"始终表示一个不会
被绕过的界,调用方要运行剩余事件需显式提高上限。
时间线语义
apsis-sim 把计划展开为时间线上的起止事件:每个已放置任务贡献一个开始
事件和一个结束事件,二者是独立事件,因此引擎能按时间顺序把它们与其他事件
正确排序,重叠任务的先后也由时钟决定。
时间线是计划本身,不是物理模型。开始/结束事件触发时调用什么物理计算由处理 函数决定,门面只负责排序与记录。
场景文本格式
apsis-scenario 自带一套零依赖的行式文本格式,用于存储与重新加载。
解析分三遍(资源、任务、优先级约束),因此 require 可以引用文件中稍后
定义的资源,precedence 可以引用任意顺序定义的任务。每处失败都带行号。
Scenario::to_text 的输出必须能被 Scenario::parse 读回为相等场景,
即文本形式是往返稳定的;这一点由测试保证。
任务的"无窗口"(两端为无穷)无法写入文本,故写为一个包含该任务的有限区间, 读回后任务仍可用。
确定性
仿真结果必须可复现,这是对拍的前提。确定性来自三点:
- 事件队列表述的同刻顺序规则;
- 规划器对同权任务的稳定排序(见 ops 层);
- 记录器保留插入顺序,
Trace::in_time_order为稳定排序。
integrate_sim 示例断言同一次准备出的仿真重复运行得到相同轨迹。