use crate::INF_WEIGHT;
use crate::bundle::INVALID_ID;
use crate::structure::Cch;
use rayon::prelude::*;
use std::cell::RefCell;
thread_local! {
static ARC_ID_CACHE: RefCell<Vec<u32>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Metric {
pub forward: Vec<u32>,
pub backward: Vec<u32>,
}
impl Metric {
#[must_use]
pub fn view(&self) -> crate::bundle::MetricView<'_> {
crate::bundle::MetricView {
forward: &self.forward,
backward: &self.backward,
}
}
}
pub(crate) struct Levels {
pub nodes: Vec<u32>,
pub first: Vec<u32>,
}
pub(crate) fn compute_levels(cch: &Cch) -> Levels {
let n = cch.node_count();
let mut height = vec![0u32; n];
let mut num_levels = 1u32; for (x, &p) in cch.elimination_tree_parent.iter().enumerate() {
if p != INVALID_ID {
let cand = height[x] + 1;
if cand > height[p as usize] {
height[p as usize] = cand;
}
}
if height[x] + 1 > num_levels {
num_levels = height[x] + 1;
}
}
let mut first = vec![0u32; num_levels as usize + 1];
for &h in &height {
first[h as usize + 1] += 1;
}
for l in 0..num_levels as usize {
first[l + 1] += first[l];
}
let mut cursor: Vec<u32> = first[..num_levels as usize].to_vec();
let mut nodes = vec![0u32; n];
#[allow(clippy::cast_possible_truncation)] for (x, &h) in height.iter().enumerate() {
let l = h as usize;
nodes[cursor[l] as usize] = x as u32;
cursor[l] += 1;
}
Levels { nodes, first }
}
#[inline]
fn add(a: u32, b: u32) -> u32 {
a.wrapping_add(b)
}
#[inline]
fn min_to(x: &mut u32, y: u32) {
if y < *x {
*x = y;
}
}
struct DisjointArcs {
forward: *mut u32,
backward: *mut u32,
len: usize,
}
unsafe impl Send for DisjointArcs {}
unsafe impl Sync for DisjointArcs {}
unsafe fn relax_node(x: usize, cache: &mut [u32], arcs: &DisjointArcs, cch: &Cch) {
for xz_up in cch.up_first_out[x]..cch.up_first_out[x + 1] {
cache[cch.up_head[xz_up as usize] as usize] = xz_up;
}
for xy_down in cch.down_first_out[x]..cch.down_first_out[x + 1] {
let bottom = cch.down_to_up[xy_down as usize] as usize;
let y = cch.down_head[xy_down as usize] as usize;
let y_up_begin = cch.up_first_out[y];
let mut cursor = cch.up_first_out[y + 1];
while cursor > y_up_begin {
cursor -= 1;
let mid = cursor as usize;
let z = cch.up_head[mid] as usize;
if z <= x {
break;
}
let top = cache[z] as usize;
debug_assert!(top < arcs.len && mid < arcs.len && bottom < arcs.len);
let bwd_bottom = unsafe { *arcs.backward.add(bottom) };
let fwd_mid = unsafe { *arcs.forward.add(mid) };
let fwd_bottom = unsafe { *arcs.forward.add(bottom) };
let bwd_mid = unsafe { *arcs.backward.add(mid) };
let fwd_candidate = add(bwd_bottom, fwd_mid);
let bwd_candidate = add(fwd_bottom, bwd_mid);
let fwd_top = unsafe { &mut *arcs.forward.add(top) };
min_to(fwd_top, fwd_candidate);
let bwd_top = unsafe { &mut *arcs.backward.add(top) };
min_to(bwd_top, bwd_candidate);
}
}
}
impl Cch {
#[must_use]
pub fn customizer(&self) -> Customizer<'_> {
let n = self.node_count();
let arc_count = self.cch_arc_count();
assert!(
self.up_head.iter().all(|&h| (h as usize) < n)
&& self.down_head.iter().all(|&h| (h as usize) < n),
"malformed Cch: head contains a node id >= node_count"
);
assert!(
self.down_to_up.iter().all(|&a| (a as usize) < arc_count),
"malformed Cch: down_to_up contains an arc id >= cch_arc_count"
);
assert_eq!(
self.up_first_out.get(n).copied(),
u32::try_from(arc_count).ok(),
"malformed Cch: up_first_out[node_count] != cch_arc_count"
);
Customizer {
cch: self,
levels: compute_levels(self),
}
}
#[must_use]
pub fn customize(&self, weights: &[u32]) -> Metric {
self.customizer().customize(weights)
}
}
pub struct Customizer<'a> {
cch: &'a Cch,
levels: Levels,
}
impl Customizer<'_> {
#[must_use]
pub fn customize(&self, weights: &[u32]) -> Metric {
let arc_count = self.cch.cch_arc_count();
let mut out = Metric {
forward: vec![INF_WEIGHT; arc_count],
backward: vec![INF_WEIGHT; arc_count],
};
self.customize_into(weights, &mut out);
out
}
pub fn customize_into(&self, weights: &[u32], out: &mut Metric) {
let cch = self.cch;
assert_eq!(
weights.len(),
cch.input_arc_to_cch_arc.len(),
"weights length must equal input arc count"
);
let arc_count = cch.cch_arc_count();
out.forward.clear();
out.forward.resize(arc_count, INF_WEIGHT);
out.backward.clear();
out.backward.resize(arc_count, INF_WEIGHT);
let forward = &mut out.forward;
let backward = &mut out.backward;
forward
.par_iter_mut()
.zip(backward.par_iter_mut())
.enumerate()
.for_each(|(cch_arc, (fwd, bwd))| {
let fwd_in = cch.forward_input_arc_of_cch[cch_arc];
if fwd_in != INVALID_ID {
*fwd = weights[fwd_in as usize];
}
let bwd_in = cch.backward_input_arc_of_cch[cch_arc];
if bwd_in != INVALID_ID {
*bwd = weights[bwd_in as usize];
}
let ef = &cch.first_extra_forward_input_arc_of_cch;
for j in ef[cch_arc]..ef[cch_arc + 1] {
let ia = cch.extra_forward_input_arc_of_cch[j as usize] as usize;
min_to(fwd, weights[ia]);
}
let eb = &cch.first_extra_backward_input_arc_of_cch;
for j in eb[cch_arc]..eb[cch_arc + 1] {
let ia = cch.extra_backward_input_arc_of_cch[j as usize] as usize;
min_to(bwd, weights[ia]);
}
});
let node_count = cch.node_count();
let arcs = DisjointArcs {
forward: forward.as_mut_ptr(),
backward: backward.as_mut_ptr(),
len: arc_count,
};
let levels = &self.levels;
for l in 0..levels.first.len() - 1 {
let level = &levels.nodes[levels.first[l] as usize..levels.first[l + 1] as usize];
level.par_iter().for_each(|&x| {
ARC_ID_CACHE.with(|c| {
let mut cache = c.borrow_mut();
if cache.len() < node_count {
cache.resize(node_count, 0);
}
unsafe { relax_node(x as usize, &mut cache, &arcs, cch) };
});
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::Graph;
fn relax_serial(cch: &Cch, weights: &[u32]) -> Metric {
let arc_count = cch.cch_arc_count();
let mut forward = vec![INF_WEIGHT; arc_count];
let mut backward = vec![INF_WEIGHT; arc_count];
for cch_arc in 0..arc_count {
let fwd_in = cch.forward_input_arc_of_cch[cch_arc];
if fwd_in != INVALID_ID {
forward[cch_arc] = weights[fwd_in as usize];
}
let bwd_in = cch.backward_input_arc_of_cch[cch_arc];
if bwd_in != INVALID_ID {
backward[cch_arc] = weights[bwd_in as usize];
}
let ef = &cch.first_extra_forward_input_arc_of_cch;
for j in ef[cch_arc]..ef[cch_arc + 1] {
min_to(
&mut forward[cch_arc],
weights[cch.extra_forward_input_arc_of_cch[j as usize] as usize],
);
}
let eb = &cch.first_extra_backward_input_arc_of_cch;
for j in eb[cch_arc]..eb[cch_arc + 1] {
min_to(
&mut backward[cch_arc],
weights[cch.extra_backward_input_arc_of_cch[j as usize] as usize],
);
}
}
let node_count = cch.node_count();
let mut cache = vec![0u32; node_count];
for x in 0..node_count {
for xz_up in cch.up_first_out[x]..cch.up_first_out[x + 1] {
cache[cch.up_head[xz_up as usize] as usize] = xz_up;
}
for xy_down in cch.down_first_out[x]..cch.down_first_out[x + 1] {
let bottom = cch.down_to_up[xy_down as usize] as usize;
let y = cch.down_head[xy_down as usize] as usize;
let y_up_begin = cch.up_first_out[y];
let mut cursor = cch.up_first_out[y + 1];
while cursor > y_up_begin {
cursor -= 1;
let mid = cursor as usize;
let z = cch.up_head[mid] as usize;
if z <= x {
break;
}
let top = cache[z] as usize;
let fc = add(backward[bottom], forward[mid]);
let bc = add(forward[bottom], backward[mid]);
min_to(&mut forward[top], fc);
min_to(&mut backward[top], bc);
}
}
}
Metric { forward, backward }
}
#[test]
fn metric_view_borrows_fields() {
let m = Metric {
forward: vec![1, 2, 3],
backward: vec![4, 5, 6],
};
let v = m.view();
assert_eq!(v.forward, &[1, 2, 3]);
assert_eq!(v.backward, &[4, 5, 6]);
}
fn csr(node_count: usize, tail: &[u32], head: &[u32]) -> Graph {
let mut counts = vec![0u32; node_count];
for &t in tail {
counts[t as usize] += 1;
}
let mut first_out = vec![0u32; node_count + 1];
for v in 0..node_count {
first_out[v + 1] = first_out[v] + counts[v];
}
let mut next: Vec<usize> = first_out[..node_count]
.iter()
.map(|&x| x as usize)
.collect();
let mut g_head = vec![0u32; head.len()];
for (&t, &h) in tail.iter().zip(head.iter()) {
g_head[next[t as usize]] = h;
next[t as usize] += 1;
}
Graph {
first_out,
head: g_head,
weight: vec![1u32; head.len()],
}
}
#[test]
fn single_arc() {
let g = csr(2, &[0], &[1]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let m = c.customize(&[42]);
assert_eq!(m.forward, vec![42]);
assert_eq!(m.backward, vec![INF_WEIGHT]);
}
#[test]
fn bidirectional_arc() {
let g = csr(2, &[0, 1], &[1, 0]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let m = c.customize(&[7, 9]);
assert_eq!(m.forward, vec![7]);
assert_eq!(m.backward, vec![9]);
}
#[test]
fn parallel_arc_min() {
let g = csr(2, &[0, 0, 1, 1], &[1, 1, 0, 0]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let m = c.customize(&[50, 9, 40, 8]);
assert_eq!(m.forward, vec![9]);
assert_eq!(m.backward, vec![8]);
}
#[test]
fn all_inf() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let inf = INF_WEIGHT;
let m = c.customize(&[inf, inf, inf, inf]);
assert!(m.forward.iter().all(|&w| w == inf));
assert!(m.backward.iter().all(|&w| w == inf));
}
#[test]
fn triangle_relaxation() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let m = c.customize(&[3, 5, 4, 6]);
assert_eq!(m.forward[2], 9);
assert_eq!(m.backward[2], 9);
}
#[test]
fn add_inf_helper() {
assert_eq!(add(1, 2), 3);
let s = add(INF_WEIGHT, INF_WEIGHT);
assert!(s > INF_WEIGHT);
}
#[test]
fn parallel_reset_min_combines() {
let g = csr(2, &[0, 0, 1, 1], &[1, 1, 0, 0]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let m = c.customizer().customize(&[50, 9, 40, 8]);
assert_eq!(m.forward, vec![9]);
assert_eq!(m.backward, vec![8]);
}
#[test]
#[should_panic(expected = "weights length must equal input arc count")]
fn wrong_weight_len_panics() {
let g = csr(2, &[0], &[1]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let _ = c.customize(&[1, 2, 3]);
}
#[test]
#[should_panic(expected = "malformed Cch: head contains a node id >= node_count")]
#[allow(clippy::cast_possible_truncation)] fn customizer_rejects_out_of_range_up_head() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let mut c = Cch::build(&g, &order);
let n = c.node_count();
c.up_head[0] = n as u32; let _ = c.customizer();
}
#[test]
#[should_panic(expected = "malformed Cch: head contains a node id >= node_count")]
#[allow(clippy::cast_possible_truncation)] fn customizer_rejects_out_of_range_down_head() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let mut c = Cch::build(&g, &order);
let n = c.node_count();
c.down_head[0] = n as u32;
let _ = c.customizer();
}
#[test]
#[should_panic(expected = "malformed Cch: down_to_up contains an arc id >= cch_arc_count")]
#[allow(clippy::cast_possible_truncation)] fn customizer_rejects_out_of_range_down_to_up() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let mut c = Cch::build(&g, &order);
let arc_count = c.cch_arc_count();
c.down_to_up[0] = arc_count as u32;
let _ = c.customizer();
}
#[test]
#[should_panic(expected = "malformed Cch: up_first_out[node_count] != cch_arc_count")]
fn customizer_rejects_bad_up_first_out_tail() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let mut c = Cch::build(&g, &order);
let last = c.up_first_out.len() - 1;
c.up_first_out[last] += 1;
let _ = c.customizer();
}
#[test]
fn customize_into_empty_matches_fresh() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let w = [3u32, 5, 4, 6];
let fresh = c.customize(&w);
let cust = c.customizer();
let mut out = Metric {
forward: Vec::new(),
backward: Vec::new(),
};
cust.customize_into(&w, &mut out);
assert_eq!(out, fresh);
}
#[test]
fn customize_into_oversized_matches_fresh() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let w = [3u32, 5, 4, 6];
let fresh = c.customize(&w);
let cust = c.customizer();
let mut out = Metric {
forward: vec![999; fresh.forward.len() + 5],
backward: vec![999; fresh.backward.len() + 5],
};
cust.customize_into(&w, &mut out);
assert_eq!(out, fresh);
}
#[test]
fn customizer_reuse_no_bleed() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let w1 = [3u32, 5, 4, 6];
let w2 = [10u32, 1, 2, 20];
let cust = c.customizer();
let mut out = c.customize(&w1); cust.customize_into(&w1, &mut out);
assert_eq!(out, c.customize(&w1));
cust.customize_into(&w2, &mut out);
assert_eq!(out, c.customize(&w2));
}
#[test]
fn levels_partition_is_valid() {
let g = csr(3, &[0, 0, 1, 2], &[1, 2, 0, 0]);
let order = vec![0u32, 1, 2];
let c = Cch::build(&g, &order);
let levels = compute_levels(&c);
let mut seen = levels.nodes.clone();
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2]);
assert_eq!(*levels.first.first().unwrap(), 0);
assert_eq!(*levels.first.last().unwrap(), 3);
let mut level_of = vec![0u32; c.node_count()];
#[allow(clippy::cast_possible_truncation)] for l in 0..levels.first.len() - 1 {
for &x in &levels.nodes[levels.first[l] as usize..levels.first[l + 1] as usize] {
level_of[x as usize] = l as u32;
}
}
for x in 0..c.node_count() {
for d in c.down_first_out[x]..c.down_first_out[x + 1] {
let y = c.down_head[d as usize] as usize;
assert!(
level_of[y] < level_of[x],
"down-neighbor must be lower level"
);
}
}
}
fn grid(side: u32) -> Graph {
let n = side * side;
let mut tail = Vec::new();
let mut head = Vec::new();
let idx = |r: u32, col: u32| r * side + col;
for r in 0..side {
for col in 0..side {
if col + 1 < side {
tail.push(idx(r, col));
head.push(idx(r, col + 1));
tail.push(idx(r, col + 1));
head.push(idx(r, col));
}
if r + 1 < side {
tail.push(idx(r, col));
head.push(idx(r + 1, col));
tail.push(idx(r + 1, col));
head.push(idx(r, col));
}
}
}
csr(n as usize, &tail, &head)
}
#[test]
#[allow(clippy::cast_possible_truncation)] fn parallel_relax_equals_serial_reference() {
let g = grid(5);
let order: Vec<u32> = (0..(g.first_out.len() as u32 - 1)).collect();
let c = Cch::build(&g, &order);
let input_arcs = c.input_arc_to_cch_arc.len();
for seed in [1u32, 7, 13, 99] {
let weights: Vec<u32> = (0..input_arcs as u32)
.map(|i| 1 + (i.wrapping_mul(2_654_435_761).wrapping_add(seed) % 97))
.collect();
let parallel = c.customize(&weights);
let reference = relax_serial(&c, &weights);
assert_eq!(parallel.forward, reference.forward, "seed {seed} forward");
assert_eq!(
parallel.backward, reference.backward,
"seed {seed} backward"
);
}
}
#[test]
#[allow(clippy::cast_possible_truncation)] fn parallel_relax_is_deterministic() {
let g = grid(5);
let order: Vec<u32> = (0..(g.first_out.len() as u32 - 1)).collect();
let c = Cch::build(&g, &order);
let weights: Vec<u32> = (0..c.input_arc_to_cch_arc.len() as u32)
.map(|i| 1 + i % 50)
.collect();
let a = c.customize(&weights);
let b = c.customize(&weights);
assert_eq!(a, b);
}
#[test]
fn relax_serial_matches_parallel_with_parallel_arcs() {
let g = csr(2, &[0, 0, 1, 1], &[1, 1, 0, 0]);
let order = vec![0u32, 1];
let c = Cch::build(&g, &order);
let weights = [50u32, 9, 40, 8];
let parallel = c.customize(&weights);
let reference = relax_serial(&c, &weights);
assert_eq!(parallel.forward, reference.forward);
assert_eq!(parallel.backward, reference.backward);
}
}