use std::os::raw::c_int;
use super::{Budget, BudgetKind, TimeoutBehavior, duration_ms};
use crate::{Error, Graph, TdBag, TreeDecomposition};
mod ffi {
use std::os::raw::c_int;
#[repr(C)]
pub(super) struct TdResult {
_private: [u8; 0],
}
unsafe extern "C" {
pub(super) fn td_compute(
num_nodes: c_int,
num_edges: c_int,
edges: *const c_int,
steps: i64,
iterations: c_int,
iterations_done: *mut i64,
greedy_touches: *mut i64,
unit_budget: i64,
units_per_iteration: i64,
) -> *mut TdResult;
pub(super) fn td_compute_timed_patience(
num_nodes: c_int,
num_edges: c_int,
edges: *const c_int,
steps: i64,
iterations: c_int,
timeout_ms: i64,
patience_ms: i64,
patience_unit_budget: i64,
adapt_search: c_int,
iterations_done: *mut i64,
greedy_touches: *mut i64,
unit_budget: i64,
units_per_iteration: i64,
) -> *mut TdResult;
pub(super) fn td_num_bags(td: *const TdResult) -> c_int;
pub(super) fn td_bag_size(td: *const TdResult, bag_index: c_int) -> c_int;
pub(super) fn td_bag_vertices(td: *const TdResult, bag_index: c_int, out: *mut c_int);
pub(super) fn td_bag_num_neighbors(td: *const TdResult, bag_index: c_int) -> c_int;
pub(super) fn td_bag_neighbors(td: *const TdResult, bag_index: c_int, out: *mut c_int);
pub(super) fn td_free(td: *mut TdResult);
}
}
struct NativeDecomposition(*mut ffi::TdResult);
impl Drop for NativeDecomposition {
fn drop(&mut self) {
unsafe { ffi::td_free(self.0) };
}
}
impl NativeDecomposition {
fn compute(num_vertices: u32, flat_edges: &[c_int], budget: Budget) -> Option<Self> {
let num_edges = flat_edges.len() / 2;
let vertices = u64::from(num_vertices);
let elements = graph_elements(vertices, num_edges as u64);
let metered = crate::meter::is_armed();
let (unit_budget, patience_unit_budget) = match budget.kind {
BudgetKind::Timed {
timeout, patience, ..
} if metered => {
let timeout = duration_work_units(timeout);
let patience = patience.map(duration_work_units).unwrap_or(0);
(timeout, patience)
}
_ => (0, 0),
};
let setup_units = SETUP_UNITS_PER_ELEMENT.saturating_mul(elements);
if unit_budget > 0 && setup_units >= unit_budget as u64 {
crate::meter::charge(DECLINE_UNITS_PER_ELEMENT.saturating_mul(elements));
return None;
}
let search_units = (unit_budget.max(0) as u64).saturating_sub(setup_units);
let search_budget = i64::try_from(search_units).unwrap_or(i64::MAX);
let units_per_iteration =
i64::try_from(iteration_work_units(vertices, num_edges as u64)).unwrap_or(i64::MAX);
let mut iterations_done = 0i64;
let mut greedy_touches = 0i64;
let raw = match budget.kind {
BudgetKind::Timed {
timeout,
patience,
iterations,
steps,
timeout_behavior,
} => {
let timeout_ms = duration_ms(timeout);
let patience_ms = patience.map(duration_ms).unwrap_or(0);
let iterations = i32::try_from(iterations)
.expect("public FlowCutter validation bounds iterations");
let steps =
i64::try_from(steps).expect("public FlowCutter validation bounds steps");
let steps = match timeout_behavior {
TimeoutBehavior::AdaptSearch => steps,
TimeoutBehavior::StopOnly => scaled_steps(steps, num_edges),
};
unsafe {
ffi::td_compute_timed_patience(
num_vertices as c_int,
num_edges as c_int,
flat_edges.as_ptr(),
steps,
iterations,
timeout_ms,
patience_ms,
patience_unit_budget,
timeout_behavior.as_ffi(),
&mut iterations_done,
&mut greedy_touches,
search_budget,
units_per_iteration,
)
}
}
BudgetKind::Steps { steps, iterations } => {
let steps =
i64::try_from(steps).expect("public FlowCutter validation bounds steps");
let iterations = i32::try_from(iterations)
.expect("public FlowCutter validation bounds iterations");
unsafe {
ffi::td_compute(
num_vertices as c_int,
num_edges as c_int,
flat_edges.as_ptr(),
scaled_steps(steps, num_edges),
iterations,
&mut iterations_done,
&mut greedy_touches,
search_budget,
units_per_iteration,
)
}
}
};
charge_build(vertices, num_edges as u64, iterations_done, greedy_touches);
(!raw.is_null()).then_some(Self(raw))
}
}
fn duration_work_units(duration: std::time::Duration) -> i64 {
let milliseconds = duration_ms(duration) as u64;
let units = milliseconds.saturating_mul(crate::meter::UNITS_PER_MS);
i64::try_from(units).unwrap_or(i64::MAX)
}
pub(super) fn run(graph: &Graph, budget: Budget) -> Result<TreeDecomposition, Error> {
let mut flat_edges = Vec::with_capacity(graph.edges.len() * 2);
for &(left, right) in &graph.edges {
flat_edges.push(left as c_int);
flat_edges.push(right as c_int);
}
let native = NativeDecomposition::compute(graph.num_vertices, &flat_edges, budget)
.ok_or(Error::NoDecomposition)?;
extract(&native, graph)
}
fn extract(native: &NativeDecomposition, graph: &Graph) -> Result<TreeDecomposition, Error> {
unsafe {
let num_bags = ffi::td_num_bags(native.0);
if num_bags == 0 {
return Err(Error::NoDecomposition);
}
if num_bags < 0 {
return Err(Error::InvalidDecomposition(
"FlowCutter returned a negative bag count".into(),
));
}
let num_bags = num_bags as usize;
let mut bags = Vec::with_capacity(num_bags);
let mut adjacency = Vec::with_capacity(num_bags);
for bag_index in 0..num_bags {
let bag_size = ffi::td_bag_size(native.0, bag_index as c_int);
if bag_size < 0 || bag_size as u32 > graph.num_vertices {
return Err(Error::InvalidDecomposition(format!(
"FlowCutter bag {bag_index} has invalid size {bag_size}"
)));
}
let mut vertices = vec![0; bag_size as usize];
ffi::td_bag_vertices(native.0, bag_index as c_int, vertices.as_mut_ptr());
let vertices = vertices
.into_iter()
.map(|vertex| {
u32::try_from(vertex)
.ok()
.filter(|&vertex| vertex < graph.num_vertices)
.ok_or_else(|| {
Error::InvalidDecomposition(format!(
"FlowCutter bag {bag_index} contains invalid vertex {vertex}"
))
})
})
.collect::<Result<Vec<_>, _>>()?;
bags.push(TdBag::from_algorithm_order(vertices));
let neighbor_count = ffi::td_bag_num_neighbors(native.0, bag_index as c_int);
if neighbor_count < 0 || neighbor_count as usize > num_bags {
return Err(Error::InvalidDecomposition(format!(
"FlowCutter bag {bag_index} has invalid neighbor count {neighbor_count}"
)));
}
let mut neighbors = vec![0; neighbor_count as usize];
ffi::td_bag_neighbors(native.0, bag_index as c_int, neighbors.as_mut_ptr());
let neighbors = neighbors
.into_iter()
.map(|neighbor| {
usize::try_from(neighbor)
.ok()
.filter(|&neighbor| neighbor < num_bags)
.ok_or_else(|| {
Error::InvalidDecomposition(format!(
"FlowCutter bag {bag_index} has invalid neighbor {neighbor}"
))
})
})
.collect::<Result<Vec<_>, _>>()?;
adjacency.push(neighbors);
}
let adjacency = reconstruct_native_adjacency(&adjacency);
let decomposition = TreeDecomposition::from_parts(graph.num_vertices, bags, adjacency);
decomposition.validate(graph)?;
Ok(decomposition)
}
}
pub(super) fn reconstruct_native_adjacency(native: &[Vec<usize>]) -> Vec<Vec<usize>> {
let mut adjacency = vec![Vec::new(); native.len()];
for (bag, neighbours) in native.iter().enumerate() {
for &neighbour in neighbours {
if neighbour > bag {
adjacency[bag].push(neighbour);
adjacency[neighbour].push(bag);
}
}
}
adjacency
}
const SETUP_UNITS_PER_ELEMENT: u64 = 6_000;
const ITERATION_UNITS_PER_FLOW: u64 = 50;
const DECLINE_UNITS_PER_ELEMENT: u64 = 20;
fn graph_elements(vertices: u64, edges: u64) -> u64 {
vertices.saturating_add(edges.saturating_mul(2))
}
pub(crate) fn iteration_work_units(vertices: u64, edges: u64) -> u64 {
ITERATION_UNITS_PER_FLOW
.saturating_mul(edges.saturating_mul(2))
.saturating_mul(vertices.isqrt())
}
fn charge_build(vertices: u64, edges: u64, iterations_done: i64, greedy_touches: i64) {
if !crate::meter::is_armed() {
return;
}
let setup = SETUP_UNITS_PER_ELEMENT.saturating_mul(graph_elements(vertices, edges));
let search =
(iterations_done.max(0) as u64).saturating_mul(iteration_work_units(vertices, edges));
crate::meter::charge(
setup
.saturating_add(search)
.saturating_add(greedy_touches.max(0) as u64),
);
}
fn scaled_steps(steps: i64, num_edges: usize) -> i64 {
steps.min(10_000i64.max(50 * num_edges as i64))
}