1#![allow(clippy::missing_errors_doc)]
7
8use canwu_time::{SimDuration, SimTime};
9use serde::{Deserialize, Serialize};
10use std::cmp::Ordering;
11use std::collections::{BTreeMap, BTreeSet, BinaryHeap, VecDeque};
12use std::fmt;
13
14pub const ROUTING_ALGORITHM_VERSION: &str = "canwu-routing.v1";
15
16#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
17#[serde(transparent)]
18pub struct RoutingNodeRef(String);
19
20impl RoutingNodeRef {
21 #[must_use]
22 pub fn new(value: impl Into<String>) -> Self {
23 Self(value.into())
24 }
25
26 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30}
31
32#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
33#[serde(transparent)]
34pub struct RoutingConnectionRef(String);
35
36impl RoutingConnectionRef {
37 #[must_use]
38 pub fn new(value: impl Into<String>) -> Self {
39 Self(value.into())
40 }
41
42 #[must_use]
43 pub fn as_str(&self) -> &str {
44 &self.0
45 }
46}
47
48#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum RoutingEndpointKind {
51 Settlement,
52 RelayStation,
53 RailwayStation,
54 Port,
55 Airport,
56 TelegraphOffice,
57 DeliveryDistrict,
58 MilitaryPosition,
59}
60
61#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
62#[serde(rename_all = "snake_case")]
63pub enum TransferMode {
64 Foot,
65 Horse,
66 RoadVehicle,
67 RiverBoat,
68 Sea,
69 Rail,
70 Air,
71 Signal,
72}
73
74#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
75pub struct RoutingEndpoint {
76 pub id: RoutingNodeRef,
77 pub kind: RoutingEndpointKind,
78}
79
80#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81pub struct DepartureSlot {
82 pub departure_at: SimTime,
83 pub duration: SimDuration,
84}
85
86#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
87pub struct DurationSample {
88 pub from: SimTime,
89 pub duration: SimDuration,
90}
91
92#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
93#[serde(tag = "type", rename_all = "snake_case")]
94pub enum TraversalModel {
95 Fixed { duration: SimDuration },
96 Departures { slots: Vec<DepartureSlot> },
97 Piecewise { samples: Vec<DurationSample> },
98}
99
100impl TraversalModel {
101 fn validate(&self) -> Result<(), RoutingError> {
102 match self {
103 Self::Fixed { duration } if duration.is_negative() => Err(
104 RoutingError::InvalidNetwork("negative traversal duration".to_owned()),
105 ),
106 Self::Fixed { .. } => Ok(()),
107 Self::Departures { slots } => {
108 if slots
109 .windows(2)
110 .any(|pair| pair[0].departure_at >= pair[1].departure_at)
111 || slots.iter().any(|slot| slot.duration.is_negative())
112 {
113 return Err(RoutingError::InvalidNetwork(
114 "departure slots must be sorted and non-negative".to_owned(),
115 ));
116 }
117 Ok(())
118 }
119 Self::Piecewise { samples } => {
120 if samples.windows(2).any(|pair| pair[0].from >= pair[1].from)
121 || samples.iter().any(|sample| sample.duration.is_negative())
122 {
123 return Err(RoutingError::InvalidNetwork(
124 "duration samples must be sorted and non-negative".to_owned(),
125 ));
126 }
127 Ok(())
128 }
129 }
130 }
131
132 fn traverse_after(&self, at: SimTime) -> Option<(SimTime, SimTime)> {
133 match self {
134 Self::Fixed { duration } => Some((at, at.checked_add(*duration)?)),
135 Self::Departures { slots } => slots.iter().find_map(|slot| {
136 (slot.departure_at >= at).then(|| {
137 Some((
138 slot.departure_at,
139 slot.departure_at.checked_add(slot.duration)?,
140 ))
141 })?
142 }),
143 Self::Piecewise { samples } => samples
144 .iter()
145 .rev()
146 .find(|sample| sample.from <= at)
147 .and_then(|sample| Some((at, at.checked_add(sample.duration)?))),
148 }
149 }
150}
151
152#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
153pub struct RoutingConnection {
154 pub id: RoutingConnectionRef,
155 pub from: RoutingNodeRef,
156 pub to: RoutingNodeRef,
157 pub mode: TransferMode,
158 pub traversal: TraversalModel,
159 pub available_from: Option<SimTime>,
160 pub available_until: Option<SimTime>,
161 pub risk_per_mille: u32,
162 pub resource_cost: u64,
163}
164
165impl RoutingConnection {
166 fn validate(&self, endpoints: &BTreeSet<RoutingNodeRef>) -> Result<(), RoutingError> {
167 if !endpoints.contains(&self.from) || !endpoints.contains(&self.to) || self.from == self.to
168 {
169 return Err(RoutingError::InvalidNetwork(format!(
170 "connection {} has invalid endpoints",
171 self.id.0
172 )));
173 }
174 if self.risk_per_mille > 1_000 {
175 return Err(RoutingError::InvalidNetwork(format!(
176 "connection {} risk exceeds 1000 per mille",
177 self.id.0
178 )));
179 }
180 if let (Some(start), Some(end)) = (self.available_from, self.available_until)
181 && start > end
182 {
183 return Err(RoutingError::InvalidNetwork(format!(
184 "connection {} availability is inverted",
185 self.id.0
186 )));
187 }
188 self.traversal.validate()
189 }
190
191 fn traverse_after(&self, at: SimTime) -> Option<(SimTime, SimTime)> {
192 let at = self.available_from.map_or(at, |start| at.max(start));
193 if self.available_until.is_some_and(|end| at > end) {
194 return None;
195 }
196 let result = self.traversal.traverse_after(at)?;
197 self.available_until
198 .is_none_or(|end| result.1 <= end)
199 .then_some(result)
200 }
201}
202
203#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
204pub struct RoutingNetwork {
205 pub version: String,
206 pub endpoints: Vec<RoutingEndpoint>,
207 pub connections: Vec<RoutingConnection>,
208}
209
210impl RoutingNetwork {
211 pub fn new(
212 version: impl Into<String>,
213 mut endpoints: Vec<RoutingEndpoint>,
214 mut connections: Vec<RoutingConnection>,
215 ) -> Result<Self, RoutingError> {
216 endpoints.sort_by(|left, right| left.id.cmp(&right.id));
217 connections.sort_by(|left, right| left.id.cmp(&right.id));
218 let endpoint_ids = endpoints
219 .iter()
220 .map(|endpoint| endpoint.id.clone())
221 .collect::<Vec<_>>();
222 if endpoint_ids.windows(2).any(|pair| pair[0] == pair[1]) {
223 return Err(RoutingError::InvalidNetwork(
224 "duplicate endpoint".to_owned(),
225 ));
226 }
227 let endpoint_set = endpoint_ids.iter().cloned().collect::<BTreeSet<_>>();
228 if connections.windows(2).any(|pair| pair[0].id == pair[1].id) {
229 return Err(RoutingError::InvalidNetwork(
230 "duplicate connection".to_owned(),
231 ));
232 }
233 for connection in &connections {
234 connection.validate(&endpoint_set)?;
235 }
236 Ok(Self {
237 version: version.into(),
238 endpoints,
239 connections,
240 })
241 }
242}
243
244#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
245pub struct PlanningSnapshot {
246 pub observer: String,
247 pub observed_at: SimTime,
248 pub valid_until: Option<SimTime>,
249 pub knowledge_cut: String,
250 pub topology_version: String,
251 pub timetable_version: Option<String>,
252 pub network: RoutingNetwork,
253}
254
255impl PlanningSnapshot {
256 pub fn validate(&self) -> Result<(), RoutingError> {
257 if self
258 .valid_until
259 .is_some_and(|until| until < self.observed_at)
260 {
261 return Err(RoutingError::InvalidSnapshot(
262 "planning snapshot expires before it is observed".to_owned(),
263 ));
264 }
265 if self.topology_version != self.network.version {
266 return Err(RoutingError::InvalidSnapshot(
267 "topology version does not match network version".to_owned(),
268 ));
269 }
270 Ok(())
271 }
272
273 #[must_use]
274 pub fn digest(&self) -> String {
275 canonical_digest(self)
276 }
277}
278
279#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
280#[serde(rename_all = "snake_case")]
281pub enum RoutingAlgorithm {
282 FifoDijkstraV1,
283 BoundedLabelCorrectingV1,
284}
285
286#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
287pub struct RoutingPolicy {
288 pub version: String,
289 pub algorithm: RoutingAlgorithm,
290 pub allowed_modes: BTreeSet<TransferMode>,
291 pub max_arrival_at: Option<SimTime>,
292 pub max_expanded_nodes: usize,
293 pub max_transfers: usize,
294 pub max_risk_per_mille: u64,
295}
296
297impl Default for RoutingPolicy {
298 fn default() -> Self {
299 Self {
300 version: "canwu-routing.policy.v1".to_owned(),
301 algorithm: RoutingAlgorithm::FifoDijkstraV1,
302 allowed_modes: BTreeSet::new(),
303 max_arrival_at: None,
304 max_expanded_nodes: 10_000,
305 max_transfers: 64,
306 max_risk_per_mille: 1_000,
307 }
308 }
309}
310
311impl RoutingPolicy {
312 fn allows(&self, mode: TransferMode) -> bool {
313 self.allowed_modes.is_empty() || self.allowed_modes.contains(&mode)
314 }
315}
316
317#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
318pub struct RoutingRequest {
319 pub origin: RoutingNodeRef,
320 pub destination: RoutingNodeRef,
321 pub departure_at: SimTime,
322 pub policy: RoutingPolicy,
323}
324
325#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
326pub struct RouteLeg {
327 pub connection: RoutingConnectionRef,
328 pub from: RoutingNodeRef,
329 pub to: RoutingNodeRef,
330 pub mode: TransferMode,
331 pub planned_departure_at: SimTime,
332 pub planned_arrival_at: SimTime,
333}
334
335#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
336pub struct RouteCost {
337 pub estimated_arrival_at: SimTime,
338 pub risk_per_mille: u64,
339 pub resource_cost: u64,
340 pub transfers: usize,
341}
342
343#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
344pub struct RoutePlan {
345 pub algorithm_version: String,
346 pub policy_version: String,
347 pub planning_snapshot_digest: String,
348 pub origin: RoutingNodeRef,
349 pub destination: RoutingNodeRef,
350 pub departure_at: SimTime,
351 pub estimated_arrival_at: SimTime,
352 pub cost: RouteCost,
353 pub legs: Vec<RouteLeg>,
354 pub digest: String,
355}
356
357#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
358pub enum RoutingError {
359 InvalidNetwork(String),
360 InvalidSnapshot(String),
361 UnknownOrigin,
362 UnknownDestination,
363 NoKnownRoute,
364 SearchHorizonExceeded,
365 ExpansionBudgetExceeded,
366 RequirementsUnsatisfied,
367 ArithmeticOverflow,
368}
369
370impl fmt::Display for RoutingError {
371 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
372 write!(formatter, "{self:?}")
373 }
374}
375
376impl std::error::Error for RoutingError {}
377
378#[derive(Clone, Debug, Eq, PartialEq)]
379struct Label {
380 node: RoutingNodeRef,
381 arrival_at: SimTime,
382 risk_per_mille: u64,
383 resource_cost: u64,
384 transfers: usize,
385 legs: Vec<RouteLeg>,
386}
387
388impl Label {
389 fn key(&self) -> (&SimTime, u64, u64, usize, &Vec<RouteLeg>) {
390 (
391 &self.arrival_at,
392 self.risk_per_mille,
393 self.resource_cost,
394 self.transfers,
395 &self.legs,
396 )
397 }
398}
399
400#[derive(Clone, Debug, Eq, PartialEq)]
401struct QueueEntry {
402 node: RoutingNodeRef,
403 arrival_at: SimTime,
404 risk_per_mille: u64,
405 resource_cost: u64,
406 transfers: usize,
407 legs: Vec<RouteLeg>,
408}
409
410impl QueueEntry {
411 fn from_label(label: &Label) -> Self {
412 Self {
413 node: label.node.clone(),
414 arrival_at: label.arrival_at,
415 risk_per_mille: label.risk_per_mille,
416 resource_cost: label.resource_cost,
417 transfers: label.transfers,
418 legs: label.legs.clone(),
419 }
420 }
421
422 fn key(&self) -> (&SimTime, u64, u64, usize, &RoutingNodeRef, &Vec<RouteLeg>) {
423 (
424 &self.arrival_at,
425 self.risk_per_mille,
426 self.resource_cost,
427 self.transfers,
428 &self.node,
429 &self.legs,
430 )
431 }
432}
433
434impl Ord for QueueEntry {
435 fn cmp(&self, other: &Self) -> Ordering {
436 other.key().cmp(&self.key())
437 }
438}
439
440impl PartialOrd for QueueEntry {
441 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
442 Some(self.cmp(other))
443 }
444}
445
446fn better(left: &Label, right: &Label) -> bool {
447 left.key() < right.key()
448}
449
450fn outgoing<'a>(
451 network: &'a RoutingNetwork,
452 node: &RoutingNodeRef,
453) -> impl Iterator<Item = &'a RoutingConnection> {
454 network
455 .connections
456 .iter()
457 .filter(move |connection| &connection.from == node)
458}
459
460fn extend(label: &Label, connection: &RoutingConnection, policy: &RoutingPolicy) -> Option<Label> {
461 if !policy.allows(connection.mode) || label.transfers >= policy.max_transfers {
462 return None;
463 }
464 let (departure_at, arrival_at) = connection.traverse_after(label.arrival_at)?;
465 let risk_per_mille = label
466 .risk_per_mille
467 .checked_add(u64::from(connection.risk_per_mille))?;
468 let resource_cost = label.resource_cost.checked_add(connection.resource_cost)?;
469 let transfers = label.transfers + 1;
470 if risk_per_mille > policy.max_risk_per_mille
471 || policy
472 .max_arrival_at
473 .is_some_and(|limit| arrival_at > limit)
474 {
475 return None;
476 }
477 let mut legs = label.legs.clone();
478 legs.push(RouteLeg {
479 connection: connection.id.clone(),
480 from: connection.from.clone(),
481 to: connection.to.clone(),
482 mode: connection.mode,
483 planned_departure_at: departure_at,
484 planned_arrival_at: arrival_at,
485 });
486 Some(Label {
487 node: connection.to.clone(),
488 arrival_at,
489 risk_per_mille,
490 resource_cost,
491 transfers,
492 legs,
493 })
494}
495
496fn solve_dijkstra(
497 snapshot: &PlanningSnapshot,
498 request: &RoutingRequest,
499) -> Result<Label, RoutingError> {
500 let mut labels = BTreeMap::<RoutingNodeRef, Label>::new();
501 let start = Label {
502 node: request.origin.clone(),
503 arrival_at: request.departure_at,
504 risk_per_mille: 0,
505 resource_cost: 0,
506 transfers: 0,
507 legs: Vec::new(),
508 };
509 labels.insert(request.origin.clone(), start.clone());
510 let mut queue = BinaryHeap::from([QueueEntry::from_label(&start)]);
511 let mut expanded = 0;
512 while let Some(entry) = queue.pop() {
513 expanded += 1;
514 if expanded > request.policy.max_expanded_nodes {
515 return Err(RoutingError::ExpansionBudgetExceeded);
516 }
517 let Some(current) = labels.get(&entry.node).cloned() else {
518 continue;
519 };
520 if current.arrival_at != entry.arrival_at
521 || current.risk_per_mille != entry.risk_per_mille
522 || current.resource_cost != entry.resource_cost
523 || current.transfers != entry.transfers
524 || current.legs != entry.legs
525 {
526 continue;
528 }
529 if entry.node == request.destination {
530 return Ok(current);
531 }
532 for connection in outgoing(&snapshot.network, &entry.node) {
533 let Some(candidate) = extend(¤t, connection, &request.policy) else {
534 continue;
535 };
536 let replace = labels
537 .get(&candidate.node)
538 .is_none_or(|existing| better(&candidate, existing));
539 if replace {
540 queue.push(QueueEntry::from_label(&candidate));
541 labels.insert(candidate.node.clone(), candidate);
542 }
543 }
544 }
545 Err(RoutingError::NoKnownRoute)
546}
547
548fn solve_label_correcting(
549 snapshot: &PlanningSnapshot,
550 request: &RoutingRequest,
551) -> Result<Label, RoutingError> {
552 let mut labels = BTreeMap::<RoutingNodeRef, Vec<Label>>::new();
553 let start = Label {
554 node: request.origin.clone(),
555 arrival_at: request.departure_at,
556 risk_per_mille: 0,
557 resource_cost: 0,
558 transfers: 0,
559 legs: Vec::new(),
560 };
561 labels.insert(request.origin.clone(), vec![start.clone()]);
562 let mut queue = VecDeque::from([start]);
563 let mut expanded = 0;
564 while let Some(current) = queue.pop_front() {
565 expanded += 1;
566 if expanded > request.policy.max_expanded_nodes {
567 return Err(RoutingError::ExpansionBudgetExceeded);
568 }
569 for connection in outgoing(&snapshot.network, ¤t.node) {
570 let Some(candidate) = extend(¤t, connection, &request.policy) else {
571 continue;
572 };
573 let node_labels = labels.entry(candidate.node.clone()).or_default();
574 if node_labels.iter().any(|existing| existing == &candidate) {
575 continue;
576 }
577 node_labels.push(candidate.clone());
578 queue.push_back(candidate);
579 }
580 }
581 labels
582 .remove(&request.destination)
583 .and_then(|candidates| {
584 candidates
585 .into_iter()
586 .min_by(|left, right| left.key().cmp(&right.key()))
587 })
588 .ok_or(RoutingError::NoKnownRoute)
589}
590
591pub fn plan_route(
592 snapshot: &PlanningSnapshot,
593 request: &RoutingRequest,
594) -> Result<RoutePlan, RoutingError> {
595 snapshot.validate()?;
596 if !snapshot
597 .network
598 .endpoints
599 .iter()
600 .any(|endpoint| endpoint.id == request.origin)
601 {
602 return Err(RoutingError::UnknownOrigin);
603 }
604 if !snapshot
605 .network
606 .endpoints
607 .iter()
608 .any(|endpoint| endpoint.id == request.destination)
609 {
610 return Err(RoutingError::UnknownDestination);
611 }
612 if snapshot
613 .valid_until
614 .is_some_and(|until| request.departure_at > until)
615 {
616 return Err(RoutingError::SearchHorizonExceeded);
617 }
618 if request.origin == request.destination {
619 let mut plan = RoutePlan {
620 algorithm_version: ROUTING_ALGORITHM_VERSION.to_owned(),
621 policy_version: request.policy.version.clone(),
622 planning_snapshot_digest: snapshot.digest(),
623 origin: request.origin.clone(),
624 destination: request.destination.clone(),
625 departure_at: request.departure_at,
626 estimated_arrival_at: request.departure_at,
627 cost: RouteCost {
628 estimated_arrival_at: request.departure_at,
629 ..RouteCost::default()
630 },
631 legs: Vec::new(),
632 digest: String::new(),
633 };
634 plan.digest = canonical_digest(&plan_without_digest(&plan));
635 return Ok(plan);
636 }
637 let label = match request.policy.algorithm {
638 RoutingAlgorithm::FifoDijkstraV1 => solve_dijkstra(snapshot, request)?,
639 RoutingAlgorithm::BoundedLabelCorrectingV1 => solve_label_correcting(snapshot, request)?,
640 };
641 let mut plan = RoutePlan {
642 algorithm_version: ROUTING_ALGORITHM_VERSION.to_owned(),
643 policy_version: request.policy.version.clone(),
644 planning_snapshot_digest: snapshot.digest(),
645 origin: request.origin.clone(),
646 destination: request.destination.clone(),
647 departure_at: request.departure_at,
648 estimated_arrival_at: label.arrival_at,
649 cost: RouteCost {
650 estimated_arrival_at: label.arrival_at,
651 risk_per_mille: label.risk_per_mille,
652 resource_cost: label.resource_cost,
653 transfers: label.transfers,
654 },
655 legs: label.legs,
656 digest: String::new(),
657 };
658 plan.digest = canonical_digest(&plan_without_digest(&plan));
659 Ok(plan)
660}
661
662fn plan_without_digest(
663 plan: &RoutePlan,
664) -> (
665 &str,
666 &str,
667 &str,
668 &RoutingNodeRef,
669 &RoutingNodeRef,
670 SimTime,
671 SimTime,
672 &RouteCost,
673 &Vec<RouteLeg>,
674) {
675 (
676 &plan.algorithm_version,
677 &plan.policy_version,
678 &plan.planning_snapshot_digest,
679 &plan.origin,
680 &plan.destination,
681 plan.departure_at,
682 plan.estimated_arrival_at,
683 &plan.cost,
684 &plan.legs,
685 )
686}
687
688fn canonical_digest<T: Serialize>(value: &T) -> String {
689 let bytes = serde_json::to_vec(value).expect("routing types must be serializable");
690 blake3::hash(&bytes).to_hex().to_string()
691}
692
693#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
694pub struct RoutingCache {
695 entries: BTreeMap<String, RoutePlan>,
696}
697
698impl RoutingCache {
699 #[must_use]
700 pub fn key(snapshot: &PlanningSnapshot, request: &RoutingRequest) -> String {
701 canonical_digest(&(snapshot.digest(), request))
702 }
703
704 #[must_use]
705 pub fn get(&self, key: &str) -> Option<&RoutePlan> {
706 self.entries.get(key)
707 }
708
709 pub fn insert(&mut self, key: String, plan: RoutePlan) {
710 self.entries.insert(key, plan);
711 }
712
713 pub fn clear(&mut self) {
714 self.entries.clear();
715 }
716}
717
718#[cfg(test)]
719mod tests {
720 use super::*;
721
722 fn endpoint(id: &str) -> RoutingEndpoint {
723 RoutingEndpoint {
724 id: RoutingNodeRef::new(id),
725 kind: RoutingEndpointKind::Settlement,
726 }
727 }
728
729 fn connection(id: &str, from: &str, to: &str, minutes: i64) -> RoutingConnection {
730 RoutingConnection {
731 id: RoutingConnectionRef::new(id),
732 from: RoutingNodeRef::new(from),
733 to: RoutingNodeRef::new(to),
734 mode: TransferMode::Horse,
735 traversal: TraversalModel::Fixed {
736 duration: SimDuration::minutes(minutes),
737 },
738 available_from: None,
739 available_until: None,
740 risk_per_mille: 0,
741 resource_cost: 0,
742 }
743 }
744
745 fn snapshot(connections: Vec<RoutingConnection>) -> PlanningSnapshot {
746 snapshot_with_endpoints(connections, ["a", "b", "c"])
747 }
748
749 fn snapshot_with_endpoints<const N: usize>(
750 connections: Vec<RoutingConnection>,
751 endpoint_ids: [&str; N],
752 ) -> PlanningSnapshot {
753 let network = RoutingNetwork::new(
754 "roads.v1",
755 endpoint_ids.into_iter().map(endpoint).collect(),
756 connections,
757 )
758 .unwrap();
759 PlanningSnapshot {
760 observer: "courier".to_owned(),
761 observed_at: SimTime::EPOCH,
762 valid_until: None,
763 knowledge_cut: "knowledge:1".to_owned(),
764 topology_version: "roads.v1".to_owned(),
765 timetable_version: None,
766 network,
767 }
768 }
769
770 #[test]
771 fn chooses_deterministic_earliest_arrival_across_multiple_legs() {
772 let snapshot = snapshot(vec![
773 connection("a-c", "a", "c", 90),
774 connection("a-b", "a", "b", 30),
775 connection("b-c", "b", "c", 30),
776 ]);
777 let plan = plan_route(
778 &snapshot,
779 &RoutingRequest {
780 origin: RoutingNodeRef::new("a"),
781 destination: RoutingNodeRef::new("c"),
782 departure_at: SimTime::EPOCH,
783 policy: RoutingPolicy::default(),
784 },
785 )
786 .unwrap();
787 assert_eq!(plan.estimated_arrival_at, SimTime::from_minutes(60));
788 assert_eq!(
789 plan.legs
790 .iter()
791 .map(|leg| leg.connection.clone())
792 .collect::<Vec<_>>(),
793 vec![
794 RoutingConnectionRef::new("a-b"),
795 RoutingConnectionRef::new("b-c")
796 ]
797 );
798 }
799
800 #[test]
801 fn scheduled_connections_wait_for_the_next_departure() {
802 let mut rail = connection("rail", "a", "c", 1);
803 rail.mode = TransferMode::Rail;
804 rail.traversal = TraversalModel::Departures {
805 slots: vec![DepartureSlot {
806 departure_at: SimTime::from_minutes(60),
807 duration: SimDuration::minutes(20),
808 }],
809 };
810 let snapshot = snapshot(vec![rail]);
811 let plan = plan_route(
812 &snapshot,
813 &RoutingRequest {
814 origin: RoutingNodeRef::new("a"),
815 destination: RoutingNodeRef::new("c"),
816 departure_at: SimTime::from_minutes(10),
817 policy: RoutingPolicy::default(),
818 },
819 )
820 .unwrap();
821 assert_eq!(plan.estimated_arrival_at, SimTime::from_minutes(80));
822 }
823
824 #[test]
825 fn label_correcting_handles_piecewise_duration_changes() {
826 let mut edge = connection("edge", "a", "c", 10);
827 edge.traversal = TraversalModel::Piecewise {
828 samples: vec![
829 DurationSample {
830 from: SimTime::EPOCH,
831 duration: SimDuration::minutes(100),
832 },
833 DurationSample {
834 from: SimTime::from_minutes(10),
835 duration: SimDuration::minutes(5),
836 },
837 ],
838 };
839 let snapshot = snapshot(vec![edge]);
840 let policy = RoutingPolicy {
841 algorithm: RoutingAlgorithm::BoundedLabelCorrectingV1,
842 ..RoutingPolicy::default()
843 };
844 let plan = plan_route(
845 &snapshot,
846 &RoutingRequest {
847 origin: RoutingNodeRef::new("a"),
848 destination: RoutingNodeRef::new("c"),
849 departure_at: SimTime::from_minutes(10),
850 policy,
851 },
852 )
853 .unwrap();
854 assert_eq!(plan.estimated_arrival_at, SimTime::from_minutes(15));
855 }
856
857 #[test]
858 fn label_correcting_keeps_a_later_label_for_a_faster_non_fifo_departure() {
859 let mut final_leg = connection("b-d", "b", "d", 100);
860 final_leg.traversal = TraversalModel::Piecewise {
861 samples: vec![
862 DurationSample {
863 from: SimTime::EPOCH,
864 duration: SimDuration::minutes(100),
865 },
866 DurationSample {
867 from: SimTime::from_minutes(10),
868 duration: SimDuration::minutes(1),
869 },
870 ],
871 };
872 let snapshot = snapshot_with_endpoints(
873 vec![
874 connection("a-b", "a", "b", 5),
875 connection("a-c", "a", "c", 10),
876 connection("c-b", "c", "b", 0),
877 final_leg,
878 ],
879 ["a", "b", "c", "d"],
880 );
881 let policy = RoutingPolicy {
882 algorithm: RoutingAlgorithm::BoundedLabelCorrectingV1,
883 ..RoutingPolicy::default()
884 };
885 let plan = plan_route(
886 &snapshot,
887 &RoutingRequest {
888 origin: RoutingNodeRef::new("a"),
889 destination: RoutingNodeRef::new("d"),
890 departure_at: SimTime::EPOCH,
891 policy,
892 },
893 )
894 .unwrap();
895 assert_eq!(plan.estimated_arrival_at, SimTime::from_minutes(11));
896 assert_eq!(plan.legs[1].connection, RoutingConnectionRef::new("c-b"));
897 }
898
899 #[test]
900 fn cache_key_changes_with_snapshot_or_policy() {
901 let snapshot = snapshot(vec![connection("a-c", "a", "c", 10)]);
902 let request = RoutingRequest {
903 origin: RoutingNodeRef::new("a"),
904 destination: RoutingNodeRef::new("c"),
905 departure_at: SimTime::EPOCH,
906 policy: RoutingPolicy::default(),
907 };
908 let first = RoutingCache::key(&snapshot, &request);
909 let mut changed = request.clone();
910 changed.policy.version = "policy.v2".to_owned();
911 assert_ne!(first, RoutingCache::key(&snapshot, &changed));
912 }
913}