use crate::INF_WEIGHT;
use crate::bundle::{CchView, INVALID_ID, MetricView};
enum Dir {
Fwd,
Bwd,
}
fn find_up_arc(cch: &CchView, tail: u32, head: u32) -> Option<u32> {
let from = cch.up_first_out[tail as usize];
let to = cch.up_first_out[tail as usize + 1];
(from..to).find(|&i| cch.up_head[i as usize] == head)
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::many_single_char_names)] fn unpack_arc(
cch: &CchView,
metric: &MetricView,
order: &[u32],
dir: &Dir,
x: u32,
y: u32,
xy: u32,
out: &mut Vec<u32>,
) {
let (mut a, ae) = (
cch.down_first_out[x as usize],
cch.down_first_out[x as usize + 1],
);
let (mut b, be) = (
cch.down_first_out[y as usize],
cch.down_first_out[y as usize + 1],
);
while a != ae && b != be {
let hx = cch.down_head[a as usize];
let hy = cch.down_head[b as usize];
match hx.cmp(&hy) {
std::cmp::Ordering::Less => a += 1,
std::cmp::Ordering::Greater => b += 1,
std::cmp::Ordering::Equal => {
let bottom_arc = cch.down_to_up[a as usize];
let mid_arc = cch.down_to_up[b as usize];
let z = hx;
match dir {
Dir::Fwd => {
if metric.forward[xy as usize]
== metric.backward[bottom_arc as usize]
.saturating_add(metric.forward[mid_arc as usize])
{
unpack_arc(cch, metric, order, &Dir::Bwd, z, x, bottom_arc, out);
unpack_arc(cch, metric, order, &Dir::Fwd, z, y, mid_arc, out);
return;
}
}
Dir::Bwd => {
if metric.backward[xy as usize]
== metric.forward[bottom_arc as usize]
.saturating_add(metric.backward[mid_arc as usize])
{
unpack_arc(cch, metric, order, &Dir::Bwd, z, y, mid_arc, out);
unpack_arc(cch, metric, order, &Dir::Fwd, z, x, bottom_arc, out);
return;
}
}
}
a += 1;
b += 1;
}
}
}
match dir {
Dir::Fwd => out.push(order[x as usize]),
Dir::Bwd => out.push(order[y as usize]),
}
}
pub struct PathQuery<'a> {
cch: &'a CchView<'a>,
order: Vec<u32>,
fwd_dist: Vec<u32>,
fwd_pred: Vec<u32>,
in_forward_search_space: Vec<bool>,
bwd_dist: Vec<u32>,
bwd_pred: Vec<u32>,
fwd_touched: Vec<u32>,
bwd_touched: Vec<u32>,
up_path: Vec<u32>,
}
impl<'a> PathQuery<'a> {
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn new(cch: &'a CchView<'a>) -> Self {
let n = cch.node_count() as usize;
assert!(
cch.up_head.iter().all(|&h| (h as usize) < n),
"malformed CchView: up_head contains a node id >= node_count"
);
let mut order = vec![0u32; n];
for v in 0..n {
order[cch.rank[v] as usize] = v as u32; }
Self {
cch,
order,
fwd_dist: vec![INF_WEIGHT; n],
fwd_pred: vec![INVALID_ID; n],
in_forward_search_space: vec![false; n],
bwd_dist: vec![INF_WEIGHT; n],
bwd_pred: vec![INVALID_ID; n],
fwd_touched: Vec::new(),
bwd_touched: Vec::new(),
up_path: Vec::new(),
}
}
#[must_use]
#[allow(clippy::too_many_lines)] #[allow(clippy::many_single_char_names)] pub fn path(&mut self, metric: &MetricView, source: u32, target: u32) -> Option<Vec<u32>> {
if source == target {
return Some(vec![source]);
}
for &nd in &self.fwd_touched {
let i = nd as usize;
self.fwd_dist[i] = INF_WEIGHT;
self.fwd_pred[i] = INVALID_ID;
self.in_forward_search_space[i] = false;
}
self.fwd_touched.clear();
for &nd in &self.bwd_touched {
let i = nd as usize;
self.bwd_dist[i] = INF_WEIGHT;
self.bwd_pred[i] = INVALID_ID;
}
self.bwd_touched.clear();
let cch = self.cch;
let up_first_out = cch.up_first_out;
let up_head = cch.up_head;
let elim = cch.elimination_tree_parent;
let forward = metric.forward;
let backward = metric.backward;
let s = cch.rank[source as usize];
let t = cch.rank[target as usize];
self.fwd_dist[s as usize] = 0;
self.fwd_touched.push(s);
{
let fwd_dist = &mut self.fwd_dist;
let fwd_pred = &mut self.fwd_pred;
let touched = &mut self.fwd_touched;
let mut x = s;
loop {
self.in_forward_search_space[x as usize] = true;
let dx = fwd_dist[x as usize];
if dx != INF_WEIGHT {
let from = up_first_out[x as usize] as usize;
let to = up_first_out[x as usize + 1] as usize;
let heads = &up_head[from..to];
let weights = &forward[from..to];
for (&yv, &w) in heads.iter().zip(weights) {
let y = yv as usize;
let cand = dx.saturating_add(w);
let slot = unsafe { fwd_dist.get_unchecked_mut(y) };
if cand < *slot {
*slot = cand;
unsafe { *fwd_pred.get_unchecked_mut(y) = x };
touched.push(yv);
}
}
}
let parent = elim[x as usize];
if parent == INVALID_ID {
break;
}
x = parent;
touched.push(x);
}
}
self.bwd_dist[t as usize] = 0;
self.bwd_touched.push(t);
let mut meeting = INVALID_ID;
let mut best = INF_WEIGHT;
{
let bwd_dist = &mut self.bwd_dist;
let bwd_pred = &mut self.bwd_pred;
let touched = &mut self.bwd_touched;
let mut x = t;
loop {
let dx = bwd_dist[x as usize];
if dx != INF_WEIGHT {
let from = up_first_out[x as usize] as usize;
let to = up_first_out[x as usize + 1] as usize;
let heads = &up_head[from..to];
let weights = &backward[from..to];
for (&yv, &w) in heads.iter().zip(weights) {
let y = yv as usize;
let cand = dx.saturating_add(w);
let slot = unsafe { bwd_dist.get_unchecked_mut(y) };
if cand < *slot {
*slot = cand;
unsafe { *bwd_pred.get_unchecked_mut(y) = x };
touched.push(yv);
}
}
}
if self.in_forward_search_space[x as usize] {
let fd = self.fwd_dist[x as usize];
let bd = bwd_dist[x as usize];
if fd != INF_WEIGHT && bd != INF_WEIGHT {
let l = fd.saturating_add(bd);
if l < best {
best = l;
meeting = x;
}
}
}
let parent = elim[x as usize];
if parent == INVALID_ID {
break;
}
x = parent;
touched.push(x);
}
}
if meeting == INVALID_ID || best == INF_WEIGHT {
return None;
}
let mut out: Vec<u32> = Vec::new();
let order = &self.order;
self.up_path.clear();
self.up_path.push(meeting);
{
let mut x = meeting;
while self.fwd_pred[x as usize] != INVALID_ID {
x = self.fwd_pred[x as usize];
self.up_path.push(x);
}
}
for i in (1..self.up_path.len()).rev() {
let tail = self.up_path[i]; let head = self.up_path[i - 1]; let arc = find_up_arc(cch, tail, head).expect("forward up-arc on elim path");
unpack_arc(cch, metric, order, &Dir::Fwd, tail, head, arc, &mut out);
}
{
let mut x = meeting;
let mut y = self.bwd_pred[x as usize];
while y != INVALID_ID {
let arc = find_up_arc(cch, y, x).expect("backward up-arc on elim path");
unpack_arc(cch, metric, order, &Dir::Bwd, y, x, arc, &mut out);
x = y;
y = self.bwd_pred[y as usize];
}
out.push(order[x as usize]);
}
Some(out)
}
}
#[must_use]
#[allow(clippy::too_many_lines)] #[allow(clippy::many_single_char_names)] #[allow(clippy::cast_possible_truncation)] pub fn node_path(cch: &CchView, metric: &MetricView, source: u32, target: u32) -> Option<Vec<u32>> {
if source == target {
return Some(vec![source]);
}
let n = cch.node_count() as usize;
let up_first_out = cch.up_first_out;
let up_head = cch.up_head;
let elim = cch.elimination_tree_parent;
let forward = metric.forward;
let backward = metric.backward;
let mut order = vec![0u32; n];
for v in 0..n {
order[cch.rank[v] as usize] = v as u32; }
let s = cch.rank[source as usize];
let t = cch.rank[target as usize];
let mut fwd_dist = vec![INF_WEIGHT; n];
let mut fwd_pred = vec![INVALID_ID; n];
let mut in_forward_search_space = vec![false; n];
fwd_dist[s as usize] = 0;
{
let mut x = s;
loop {
in_forward_search_space[x as usize] = true;
let dx = fwd_dist[x as usize];
if dx != INF_WEIGHT {
let from = up_first_out[x as usize] as usize;
let to = up_first_out[x as usize + 1] as usize;
let heads = &up_head[from..to];
let weights = &forward[from..to];
for (&yv, &w) in heads.iter().zip(weights) {
let y = yv as usize;
let cand = dx.saturating_add(w);
if cand < fwd_dist[y] {
fwd_dist[y] = cand;
fwd_pred[y] = x;
}
}
}
let parent = elim[x as usize];
if parent == INVALID_ID {
break;
}
x = parent;
}
}
let mut bwd_dist = vec![INF_WEIGHT; n];
let mut bwd_pred = vec![INVALID_ID; n];
bwd_dist[t as usize] = 0;
let mut meeting = INVALID_ID;
let mut best = INF_WEIGHT;
{
let mut x = t;
loop {
let dx = bwd_dist[x as usize];
if dx != INF_WEIGHT {
let from = up_first_out[x as usize] as usize;
let to = up_first_out[x as usize + 1] as usize;
let heads = &up_head[from..to];
let weights = &backward[from..to];
for (&yv, &w) in heads.iter().zip(weights) {
let y = yv as usize;
let cand = dx.saturating_add(w);
if cand < bwd_dist[y] {
bwd_dist[y] = cand;
bwd_pred[y] = x;
}
}
}
if in_forward_search_space[x as usize] {
let fd = fwd_dist[x as usize];
let bd = bwd_dist[x as usize];
if fd != INF_WEIGHT && bd != INF_WEIGHT {
let l = fd.saturating_add(bd);
if l < best {
best = l;
meeting = x;
}
}
}
let parent = cch.elimination_tree_parent[x as usize];
if parent == INVALID_ID {
break;
}
x = parent;
}
}
if meeting == INVALID_ID || best == INF_WEIGHT {
return None;
}
let mut out: Vec<u32> = Vec::new();
let mut up_path = vec![meeting];
{
let mut x = meeting;
while fwd_pred[x as usize] != INVALID_ID {
x = fwd_pred[x as usize];
up_path.push(x);
}
}
for i in (1..up_path.len()).rev() {
let tail = up_path[i]; let head = up_path[i - 1]; let arc = find_up_arc(cch, tail, head).expect("forward up-arc on elim path");
unpack_arc(cch, metric, &order, &Dir::Fwd, tail, head, arc, &mut out);
}
{
let mut x = meeting;
let mut y = bwd_pred[x as usize];
while y != INVALID_ID {
let arc = find_up_arc(cch, y, x).expect("backward up-arc on elim path");
unpack_arc(cch, metric, &order, &Dir::Bwd, y, x, arc, &mut out);
x = y;
y = bwd_pred[y as usize];
}
out.push(order[x as usize]);
}
Some(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn test_bundle_paths() -> (std::path::PathBuf, std::path::PathBuf) {
use routingkit_cch::ffi;
let mut tail = Vec::new();
let mut head = Vec::new();
for i in 0u32..9 {
tail.push(i);
head.push(i + 1);
}
tail.push(9);
head.push(0);
tail.push(0);
head.push(5);
let weights: Vec<u32> = vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100];
let order: Vec<u32> = (0u32..10).collect();
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let struct_path = tmp.path().join("test.cch-struct");
let metric_path = tmp.path().join("test.cch-metric");
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), struct_path.to_str().unwrap())
.expect("cch_save_struct");
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), metric_path.to_str().unwrap())
.expect("cch_save_metric");
}
let _ = tmp.keep();
(struct_path, metric_path)
}
fn original_arc_weight(cch: &CchView, metric: &MetricView, u: u32, v: u32) -> u64 {
let ru = cch.rank[u as usize];
let rv = cch.rank[v as usize];
let fwd = (cch.up_first_out[ru as usize]..cch.up_first_out[ru as usize + 1])
.find(|&i| cch.up_head[i as usize] == rv)
.map(|i| u64::from(metric.forward[i as usize]));
let bwd = (cch.up_first_out[rv as usize]..cch.up_first_out[rv as usize + 1])
.find(|&i| cch.up_head[i as usize] == ru)
.map(|i| u64::from(metric.backward[i as usize]));
fwd.or(bwd)
.unwrap_or_else(|| panic!("no original CCH arc between rank {ru} and {rv}"))
}
#[test]
fn path_query_endpoints_and_weight_match_distance() {
use crate::bundle::{CchBundle, MetricBundle};
use crate::query::distance_matrix;
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let (s, t) = (0u32, 5u32);
let path = node_path(&cv, &mv, s, t).expect("reachable");
assert_eq!(*path.first().unwrap(), s);
assert_eq!(*path.last().unwrap(), t);
let dist = distance_matrix(&cv, &mv, &[s], &[t])[0];
let summed: u64 = path
.windows(2)
.map(|w| original_arc_weight(&cv, &mv, w[0], w[1]))
.sum();
assert_eq!(summed, u64::from(dist));
}
#[test]
fn path_query_self_pair_is_single_node() {
use crate::bundle::{CchBundle, MetricBundle};
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let p = node_path(&cch_bundle.view(), &metric_bundle.view(), 0, 0).unwrap();
assert_eq!(p, vec![0]);
}
#[test]
fn path_query_unreachable_returns_none() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let tail = vec![0u32];
let head = vec![1u32];
let weights = vec![7u32];
let order = vec![0u32, 1];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let sp = tmp.path().join("u.cch-struct");
let mp = tmp.path().join("u.cch-metric");
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), sp.to_str().unwrap()).unwrap();
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let _ = tmp.keep();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
assert!(node_path(&cch_bundle.view(), &metric_bundle.view(), 1, 0).is_none());
}
#[test]
fn path_query_backward_arc_weight_lookup() {
use crate::bundle::{CchBundle, MetricBundle};
use crate::query::distance_matrix;
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let (s, t) = (5u32, 0u32);
let path = node_path(&cv, &mv, s, t).expect("5→0 should be reachable");
assert_eq!(*path.first().unwrap(), s);
assert_eq!(*path.last().unwrap(), t);
let dist = distance_matrix(&cv, &mv, &[s], &[t])[0];
let summed: u64 = path
.windows(2)
.map(|w| original_arc_weight(&cv, &mv, w[0], w[1]))
.sum();
assert_eq!(summed, u64::from(dist));
}
#[test]
#[should_panic(expected = "no original CCH arc between rank")]
fn original_arc_weight_panics_on_disconnected_pair() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let tail: Vec<u32> = vec![0, 1, 2, 3, 4, 3, 2, 1];
let head: Vec<u32> = vec![1, 2, 3, 4, 3, 2, 1, 0];
let weights: Vec<u32> = (1..=8u32).collect();
let n: u32 = 6;
let order: Vec<u32> = (0..n).collect();
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let cch_ref = cch.as_ref().expect("cch_new returned null");
let dir = tempfile::tempdir().expect("tempdir");
let sp = dir.path().join("iso.cch-struct");
let mp = dir.path().join("iso.cch-metric");
let mut metric = unsafe { ffi::cch_metric_new(cch_ref, &weights) };
unsafe {
ffi::cch_save_struct(cch_ref, sp.to_str().unwrap()).unwrap();
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
original_arc_weight(&cv, &mv, 5, 0);
}
#[test]
fn path_query_all_pairs_valid() {
use crate::bundle::{CchBundle, MetricBundle};
use crate::query::distance_matrix;
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let n = 10u32;
for s in 0..n {
for t in 0..n {
if s == t {
continue;
}
let dist = distance_matrix(&cv, &mv, &[s], &[t])[0];
let path = node_path(&cv, &mv, s, t).expect("reachable");
assert_eq!(*path.first().unwrap(), s);
assert_eq!(*path.last().unwrap(), t);
let summed: u64 = path
.windows(2)
.map(|w| original_arc_weight(&cv, &mv, w[0], w[1]))
.sum();
assert_eq!(summed, u64::from(dist), "path weight mismatch for {s}→{t}");
}
}
}
#[test]
fn mmap_unpack_none_for_unreachable_pair() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let tail = vec![0u32];
let head = vec![1u32];
let weights = vec![7u32];
let order = vec![0u32, 1];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let sp = tmp.path().join("none.cch-struct");
let mp = tmp.path().join("none.cch-metric");
let metric_uptr;
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), sp.to_str().unwrap()).unwrap();
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
metric_uptr = metric;
}
let _ = tmp.keep();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = unsafe { ffi::cch_query_new(metric_uptr.as_ref().unwrap()) };
unsafe {
ffi::cch_query_add_source(q.as_mut().unwrap(), 1, 0);
ffi::cch_query_add_target(q.as_mut().unwrap(), 0, 0);
ffi::cch_query_run(q.as_mut().unwrap());
}
let cpp_path = unsafe { ffi::cch_query_node_path(q.as_ref().unwrap()) };
let cpp_vec: Vec<u32> = cpp_path.clone();
assert!(
cpp_vec.is_empty(),
"oracle must return empty path for unreachable 1→0"
);
let ours = node_path(&cv, &mv, 1, 0);
assert!(ours.is_none(), "1→0 is unreachable");
let mut q2 = unsafe { ffi::cch_query_new(metric_uptr.as_ref().unwrap()) };
unsafe {
ffi::cch_query_add_source(q2.as_mut().unwrap(), 0, 0);
ffi::cch_query_add_target(q2.as_mut().unwrap(), 1, 0);
ffi::cch_query_run(q2.as_mut().unwrap());
}
let cpp_path2 = unsafe { ffi::cch_query_node_path(q2.as_ref().unwrap()) };
let cpp_vec2: Vec<u32> = cpp_path2.clone();
assert!(
!cpp_vec2.is_empty(),
"oracle must return path for reachable 0→1"
);
let ours2 = node_path(&cv, &mv, 0, 1);
assert_eq!(
ours2,
Some(cpp_vec2),
"both should agree for reachable pair 0→1"
);
}
#[test]
#[allow(clippy::cast_possible_truncation)] fn mmap_unpack_matches_cpp_reference_over_200_pairs() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let mut tail = Vec::new();
let mut head = Vec::new();
for i in 0u32..9 {
tail.push(i);
head.push(i + 1);
}
tail.push(9);
head.push(0);
tail.push(0);
head.push(5);
let weights: Vec<u32> = vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100];
let order: Vec<u32> = (0u32..10).collect();
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let sp = tmp.path().join("r200.cch-struct");
let mp = tmp.path().join("r200.cch-metric");
let metric_uptr;
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), sp.to_str().unwrap()).unwrap();
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
metric_uptr = metric;
}
let _ = tmp.keep();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let n = cv.up_first_out.len() as u32 - 1;
let mut seed: u64 = 0x9E37_79B9_7F4A_7C15;
for _ in 0..200 {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let s = ((seed >> 33) as u32) % n;
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let t = ((seed >> 33) as u32) % n;
let mut q = unsafe { ffi::cch_query_new(metric_uptr.as_ref().unwrap()) };
unsafe {
ffi::cch_query_add_source(q.as_mut().unwrap(), s, 0);
ffi::cch_query_add_target(q.as_mut().unwrap(), t, 0);
ffi::cch_query_run(q.as_mut().unwrap());
}
let cpp_path = unsafe { ffi::cch_query_node_path(q.as_ref().unwrap()) };
let cpp_vec: Vec<u32> = cpp_path.clone();
let theirs: Option<Vec<u32>> = (!cpp_vec.is_empty()).then_some(cpp_vec);
let ours = node_path(&cv, &mv, s, t);
assert_eq!(ours, theirs, "path mismatch for ({s} -> {t})");
}
}
#[test]
fn path_query_fwd_unpack_shortcut() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let tail: Vec<u32> = vec![0, 0, 1, 2, 1, 2, 3, 3];
let head: Vec<u32> = vec![1, 2, 3, 3, 0, 0, 1, 2];
let weights: Vec<u32> = vec![1u32; tail.len()];
let order: Vec<u32> = vec![0, 1, 2, 3];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let cch_ref = cch.as_ref().expect("cch_new returned null");
let dir = tempfile::tempdir().expect("tempdir");
let sp = dir.path().join("diamond.cch-struct");
let mp = dir.path().join("diamond.cch-metric");
let mut metric = unsafe { ffi::cch_metric_new(cch_ref, &weights) };
unsafe {
ffi::cch_save_struct(cch_ref, sp.to_str().unwrap()).unwrap();
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let path_12 = node_path(&cv, &mv, 1, 2).expect("1→2 reachable in diamond");
assert_eq!(*path_12.first().unwrap(), 1);
assert_eq!(*path_12.last().unwrap(), 2);
assert_eq!(path_12.len(), 3, "path 1→0→2 has 3 nodes");
let path_03 = node_path(&cv, &mv, 0, 3).expect("0→3 reachable");
assert_eq!(*path_03.first().unwrap(), 0);
assert_eq!(*path_03.last().unwrap(), 3);
}
#[test]
fn path_query_fwd_sweep_inf_weight_branch() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let n: u32 = 5;
let order: Vec<u32> = (0..n).collect();
let tail: Vec<u32> = (0..n - 1).collect();
let head: Vec<u32> = (1..n).collect();
let weights: Vec<u32> = vec![crate::INF_WEIGHT; tail.len()];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let cch_ref = cch.as_ref().expect("cch_new returned null");
let dir = tempfile::tempdir().expect("tempdir");
let sp = dir.path().join("fwdinf.cch-struct");
let mp = dir.path().join("fwdinf.cch-metric");
let mut metric = unsafe { ffi::cch_metric_new(cch_ref, &weights) };
unsafe {
ffi::cch_save_struct(cch_ref, sp.to_str().unwrap()).unwrap();
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let result = node_path(&cv, &mv, 0, 4);
assert!(result.is_none(), "all-INF metric means 0→4 is unreachable");
}
#[test]
#[should_panic(expected = "up_head contains a node id")]
fn pathquery_new_rejects_out_of_range_up_head() {
let view = CchView {
rank: &[0], elimination_tree_parent: &[INVALID_ID],
up_first_out: &[0, 1],
up_head: &[5], down_first_out: &[0, 1],
down_head: &[0],
down_to_up: &[0],
};
let _ = PathQuery::new(&view);
}
#[test]
fn pathquery_self_pair_is_single_node() {
use crate::bundle::{CchBundle, MetricBundle};
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = PathQuery::new(&cv);
assert_eq!(q.path(&mv, 0, 0).unwrap(), vec![0]);
assert_eq!(q.path(&mv, 0, 5), node_path(&cv, &mv, 0, 5));
}
#[test]
fn pathquery_unreachable_returns_none() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let tail = vec![0u32];
let head = vec![1u32];
let weights = vec![7u32];
let order = vec![0u32, 1];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let sp = tmp.path().join("pqu.cch-struct");
let mp = tmp.path().join("pqu.cch-metric");
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), sp.to_str().unwrap()).unwrap();
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let _ = tmp.keep();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = PathQuery::new(&cv);
assert!(q.path(&mv, 1, 0).is_none(), "1→0 unreachable");
assert_eq!(q.path(&mv, 0, 1), node_path(&cv, &mv, 0, 1));
}
#[test]
#[allow(clippy::cast_possible_truncation)] fn pathquery_reuse_matches_cpp_over_200_pairs() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let mut tail = Vec::new();
let mut head = Vec::new();
for i in 0u32..9 {
tail.push(i);
head.push(i + 1);
}
tail.push(9);
head.push(0);
tail.push(0);
head.push(5);
let weights: Vec<u32> = vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 100];
let order: Vec<u32> = (0u32..10).collect();
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let tmp = tempfile::TempDir::new().expect("tempdir");
let sp = tmp.path().join("pq200.cch-struct");
let mp = tmp.path().join("pq200.cch-metric");
let metric_uptr;
unsafe {
ffi::cch_save_struct(cch.as_ref().unwrap(), sp.to_str().unwrap()).unwrap();
let mut metric = ffi::cch_metric_new(cch.as_ref().unwrap(), &weights);
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
metric_uptr = metric;
}
let _ = tmp.keep();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let n = cv.up_first_out.len() as u32 - 1;
let mut q = PathQuery::new(&cv);
let mut seed: u64 = 0x9E37_79B9_7F4A_7C15;
for _ in 0..200 {
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let s = ((seed >> 33) as u32) % n;
seed = seed
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
let t = ((seed >> 33) as u32) % n;
let mut cq = unsafe { ffi::cch_query_new(metric_uptr.as_ref().unwrap()) };
unsafe {
ffi::cch_query_add_source(cq.as_mut().unwrap(), s, 0);
ffi::cch_query_add_target(cq.as_mut().unwrap(), t, 0);
ffi::cch_query_run(cq.as_mut().unwrap());
}
let cpp_vec: Vec<u32> = unsafe { ffi::cch_query_node_path(cq.as_ref().unwrap()) };
let theirs: Option<Vec<u32>> = (!cpp_vec.is_empty()).then_some(cpp_vec);
let ours = q.path(&mv, s, t);
assert_eq!(
ours, theirs,
"reused PathQuery path mismatch for ({s} -> {t})"
);
}
}
#[test]
fn pathquery_repeated_and_interleaved_pairs_match_node_path() {
use crate::bundle::{CchBundle, MetricBundle};
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = PathQuery::new(&cv);
let probes = [
(0u32, 5u32),
(0, 5), (5, 0),
(3, 8),
(0, 5), (8, 3),
(7, 2),
(3, 8), (1, 1), (2, 9),
(5, 0), ];
for &(s, t) in &probes {
assert_eq!(
q.path(&mv, s, t),
node_path(&cv, &mv, s, t),
"reused PathQuery disagrees with one-shot node_path for ({s} -> {t})"
);
}
}
#[test]
fn pathquery_all_pairs_match_node_path() {
use crate::bundle::{CchBundle, MetricBundle};
let (sp, mp) = test_bundle_paths();
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = PathQuery::new(&cv);
for s in 0..10u32 {
for t in 0..10u32 {
assert_eq!(
q.path(&mv, s, t),
node_path(&cv, &mv, s, t),
"mismatch for ({s} -> {t})"
);
}
}
}
#[test]
fn pathquery_fwd_sweep_inf_weight_branch() {
use crate::bundle::{CchBundle, MetricBundle};
use routingkit_cch::ffi;
let n: u32 = 5;
let order: Vec<u32> = (0..n).collect();
let tail: Vec<u32> = (0..n - 1).collect();
let head: Vec<u32> = (1..n).collect();
let weights: Vec<u32> = vec![crate::INF_WEIGHT; tail.len()];
let cch = unsafe { ffi::cch_new(&order, &tail, &head, |_| {}, false) };
let cch_ref = cch.as_ref().expect("cch_new returned null");
let dir = tempfile::tempdir().expect("tempdir");
let sp = dir.path().join("pqfwdinf.cch-struct");
let mp = dir.path().join("pqfwdinf.cch-metric");
let mut metric = unsafe { ffi::cch_metric_new(cch_ref, &weights) };
unsafe {
ffi::cch_save_struct(cch_ref, sp.to_str().unwrap()).unwrap();
ffi::cch_metric_customize(metric.as_mut().unwrap());
ffi::cch_save_metric(metric.as_ref().unwrap(), mp.to_str().unwrap()).unwrap();
}
let cch_bundle = CchBundle::open(&sp).unwrap();
let metric_bundle = MetricBundle::open(&mp).unwrap();
let (cv, mv) = (cch_bundle.view(), metric_bundle.view());
let mut q = PathQuery::new(&cv);
assert!(
q.path(&mv, 0, 4).is_none(),
"all-INF metric → 0→4 unreachable"
);
}
}