跳转到内容

路线规划与送信执行

本页跟随一封从无锡寄往北京的信。你会运行一次经路线规划的投递,了解 canwu-routing 如何依据某个观察者掌握的信息选择路线,以及 canwu-transport 和 canwu-movement 如何逐段记录行程,包括失败和改道。

网络和时刻表数据由你的应用提供。canwu-routing 和 canwu-transport 是普通的 Rust 库,负责计算和校验数据;你的插件把这些数据存为领域记录,并通过规范化输入(canonical ingress)修改它们。两者的类型都由 canwu-api 重新导出。

通信示例为同一位寄信人规划两次投递,一次寄给无锡本地的邻居,一次寄往北京:

Terminal window
cargo run -p canwu-correspondence --example routed_correspondence
Wuxi local delivery: 1 leg(s), arrival minute 30
wuxi/hub -> wuxi/delivery/recipient via Horse
Wuxi to Beijing: 2 leg(s), arrival minute 3060
wuxi/hub -> beijing/station via Rail
beijing/station -> beijing/delivery/recipient via Horse

寄信人知道的网络定义在 examples/support/mod.rs。本地信件只需一段 30 分钟的骑马路段。寄往北京时,路由器比较了直达列车(第 60 分钟发车,历时两天)、经南京中转的列车(三天),以及两条最后一段的骑马路线(两小时和三小时),最终选中直达列车和两小时的路线:60 + 2,880 + 120 = 第 3,060 分钟。从无锡出发的经路线规划通信完整讲解了这个示例;本页解释它底层的规划与执行契约。

“一封信怎样从无锡送到北京?”其实是两个问题:

  1. 规划:在这位观察者知道的网络、时刻表、风险和运力范围内,哪条路线能在限制条件下最早到达?
  2. 执行:信是否已经出发、到达中转站、交接给下一位承运人、因洪水改道,并最终送到收件人手里?

canwu-routing 回答第一个问题,canwu-transport 记录第二个问题的答案。收件人的知识只在信息扩展完成投递尝试时才会改变。

依据观察者知识规划路线,安装行程,预订运力,执行各路段,失败后改道. 查看图表源码.
查看图表源码
flowchart LR
  Snap["PlanningSnapshot<br/>观察者掌握的信息"] --> Plan["plan_route()<br/>RoutePlan"]
  Plan --> Install["TransportExecution<br/>初始 ItineraryRevision"]
  Install --> Book["CapacityBooking<br/>(可选)"]
  Book --> Legs["路段:出发、到达<br/>中转站 Handoff"]
  Legs -->|"路段失败"| Replan["ReplanPending"]
  Replan -->|"新的规划快照"| Reroute["后继<br/>ItineraryRevision"]
  Reroute --> Legs
  Legs -->|"最后一段到达"| Done["ArrivalPending<br/>或 Settled"]
let request = RoutingRequest {
origin: RoutingNodeRef::new("wuxi/hub"),
destination: RoutingNodeRef::new("beijing/delivery/recipient"),
departure_at: SimTime::EPOCH + SimDuration::hours(1),
policy: RoutingPolicy {
allowed_modes: [TransferMode::Rail, TransferMode::Horse]
.into_iter()
.collect(),
..RoutingPolicy::default()
},
};
let plan = plan_route(&planning_snapshot, &request)?;

PlanningSnapshot 是某位观察者在某一时刻所知道的网络,记录了 observer、observed_at、knowledge_cut、topology_version、可选的 timetable_version 和可选的过期时间 valid_until。手握最新铁路时刻表的人和只听过驿站传闻的人会得到不同的计划,而这两份计划都符合各自掌握的信息。有两个辅助函数可以构建规划快照:

  • canwu_reference_world::planning_snapshot_from_world 根据参考世界的领地和道路构建。
  • canwu_correspondence::planning_snapshot_from_holder_knowledge 只根据某个持有人账本中记录的内容构建,见按持有人知识规划路线。

得到的 RoutePlan 包含若干 legs(每段都有运输方式和计划出发、到达时间),以及 estimated_arrival_at 和 digest。近处的收件人可能只需要一小段路;去北京则可能要经过多个驿站、车站和保管权交接。

铁路、空运、电报和驿站共用一套算法,差别都放在网络数据里:

  • 铁路:TransferMode::Rail、车站节点和发车班次。1900 年与 1940 年的铁路网是两个时刻表不同的内容包。
  • 空运:TransferMode::Air、机场和航班。
  • 电报等信号:TransferMode::Signal。电报局的营业时间、延迟、风险和截获规则来自数据或你自己扩展的策略。
  • 驿站网络:中继站节点、Horse 或 Foot 连接,以及路段之间的保管权交接。

每条连接都有一个 TraversalModel:Fixed 表示固定时长,Departures 表示按班次发车,Piecewise 表示随时间变化的时长。默认算法 FifoDijkstraV1 假设晚出发就不会早到达(FIFO)。如果你的时变数据打破了这个前提,请在策略中设置 algorithm: RoutingAlgorithm::BoundedLabelCorrectingV1。

RoutingPolicy 还可以设置 max_expanded_nodes、max_transfers、max_risk_per_mille 和 max_arrival_at。NoKnownRoute、ExpansionBudgetExceeded 等失败都是确定性的结果,重放时会原样重现。

RoutingCache 以规划快照摘要和请求(含策略)为键缓存计划。它是上层应用自己的缓存,可以随时丢弃重建,重放也用不到它。

RoutePlan.estimated_arrival_at 是执行层面的预估;投递尝试的 due_at 是信息生命周期中的期限。预计会晚到时,系统记录一次失败或创建新的投递尝试,原来的期限保持不变。

改道和投递重试是两种操作:

  • 改道为同一次投递尝试创建新的不可变 ItineraryRevision。
  • 投递重试创建新的投递尝试,可以换用新的路线和运输方式。

规划完成后,上层应用创建 TransportExecution,把计划安装为第一个行程修订,并关联它要完成的投递尝试:

let mut execution = TransportExecution::new(
TransportExecutionId(7),
Some(delivery_attempt_version.clone()),
);
execution.install_initial_itinerary(initial_revision)?;
execution.begin_saga(
delivery_attempt_version,
delivery_completion_operation_key(
TransportExecutionId(7),
ItineraryRevisionId(1),
1,
),
)?;

delivery_attempt_version 是投递尝试的精确领域记录版本(DomainRecordVersionRef);initial_revision 是包含 RoutePlan、原因为 ItineraryRevisionReason::Initial 的 ItineraryRevision。begin_saga 把运输执行切换到 Executing 状态。

之后各路段依次执行:

  • start_current_leg(at) 出发,complete_current_leg(at, endpoint) 到达,每段都记录实际时间。
  • 到达中转站后,先用 record_handoff 记录保管权由谁交给谁,再开始下一段。
  • 洪水、战乱、恶劣天气或线路中断阻断路线时,领域系统调用 fail_current_leg(reason, at),运输执行进入 ReplanPending。基于新的规划快照得到后继修订,用 reroute(revision, at) 安装,再用 record_handoff 记录保管权从失败路段交到新修订的第一段。路由器只负责规划,灾害是否发生由你的系统决定。
  • 最后一段到达后,运输执行进入 ArrivalPending。completion_request() 返回一个 DeliveryCompletionRequest,其中带有稳定的操作键和证据。上层应用通过规范化输入提交它,完成信息扩展中的投递尝试。同一个键提交两次,投递尝试也只完成一次。

对运输执行 7、行程修订 1 和投递尝试版本 1,操作键为 transport/7/revision/1/delivery-completion/attempt-version/1。没有关联投递尝试的运输执行用 settle_arrival(at, endpoint) 结束,状态为 Settled。

以下记录覆盖了策略游戏中常见的移动场景:

  • MovementSubjectRole::PersonsGroup 表示人员群体:把一群人作为一个移动主体,其身份通常是应用的领域记录。它和货物一样带正整数数量,这里是人数;对这两种角色,requires_quantity() 都返回 true。
  • HandoffKind::Seizure { by } 记录扣押交接:行程之外的人夺走了保管权,例如盗匪或敌方驻军。计划交接使用 HandoffKind::Planned。扣押交接与计划交接遵循相同的路段规则。
  • 终态扣押把当前失败的路段同时作为起止两端,保管权随之离开行程。每个运输执行只允许一次终态扣押;此后它不再接受交接、路段失败、改道、到达、成功结算或预订,只能由所有者以失败或取消关闭。
  • ItineraryRevisionReason::ExternalCondition { record, version, kind } 用外部条件说明改道:引用应用自有的条件记录(例如洪水或封关记录)的某个正整数精确版本,并在 kind 中给出应用标签。初始行程和每次改道都会校验它。

运输层只记录这些事实。扣押或灾害是否发生、由谁授权,都由你的应用系统决定。

canwu-transport 保存记录,移动生命周期扩展 canwu-movement 则通过一个 MovementPlugin 推进这些记录:上层应用用带请求标识的命令 apply_movement_operation_v1 下达移动命令,应用系统通过 movement_incident_v1 报告事故,插件在各路段到期时结算,分配运力池,并发布持有人相对的报告。插件自己不抽取随机数,危险是否发生由你的系统决定。移动命令、路段、事故、预订和报告的完整规则见设计页面的运行过程。

车次、航班、驿马和电报线路都可以用 CapacityBooking 预订。它是按时间窗口持久保存的记录,状态为 Requested、Confirmed、Consumed、Released、Expired、Cancelled 或 Failed 之一。预订可以在窗口开始前确认、失败或取消,但只能在窗口内消耗。

移动执行还可以使用运力池,见运力池。通信扩展目前只提供 CorrespondenceCapacityAdmission::Unconstrained,因此通信的路段不受运力限制。