use std::os::raw::c_int;
use std::time::Duration;
use crate::{Error, Graph, TreeDecomposition};
mod native;
pub mod separator;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TimeoutBehavior {
StopOnly,
AdaptSearch,
}
impl TimeoutBehavior {
fn as_ffi(self) -> c_int {
match self {
Self::StopOnly => 0,
Self::AdaptSearch => 1,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[must_use]
pub struct Budget {
kind: BudgetKind,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BudgetKind {
Timed {
timeout: Duration,
patience: Option<Duration>,
iterations: u32,
steps: u64,
timeout_behavior: TimeoutBehavior,
},
Steps {
steps: u64,
iterations: u32,
},
}
pub(crate) const FC_TIMED_STEPS: u64 = 1_000_000;
impl Budget {
pub const fn timed(timeout: Duration, patience: Option<Duration>, iterations: u32) -> Self {
Self {
kind: BudgetKind::Timed {
timeout,
patience,
iterations,
steps: FC_TIMED_STEPS,
timeout_behavior: TimeoutBehavior::AdaptSearch,
},
}
}
pub const fn steps(steps: u64, iterations: u32) -> Self {
Self {
kind: BudgetKind::Steps { steps, iterations },
}
}
pub const fn with_timeout(
self,
timeout: Duration,
patience: Option<Duration>,
behavior: TimeoutBehavior,
) -> Self {
let (steps, iterations) = match self.kind {
BudgetKind::Timed {
steps, iterations, ..
}
| BudgetKind::Steps { steps, iterations } => (steps, iterations),
};
Self {
kind: BudgetKind::Timed {
timeout,
patience,
iterations,
steps,
timeout_behavior: behavior,
},
}
}
}
pub(super) fn duration_ms(duration: Duration) -> i64 {
duration.as_millis().max(1) as i64
}
const MAX_VERTICES: u32 = 100_000;
const MAX_EDGES: usize = 20_000_000;
fn vendor_size_guard(graph: &Graph) -> Result<(), Error> {
vendor_size_guard_counts(graph.num_vertices, graph.edges.len())
}
fn vendor_size_guard_counts(num_vertices: u32, num_edges: usize) -> Result<(), Error> {
if num_vertices > MAX_VERTICES {
return Err(Error::TooLarge(format!(
"graph too large for FlowCutter ({num_vertices} vertices; its quadratic adjacency matrix would exceed memory)"
)));
}
if num_edges > MAX_EDGES {
return Err(Error::TooLarge(format!(
"graph too dense for FlowCutter ({num_edges} edges; the backend would exceed memory)"
)));
}
Ok(())
}
pub fn decompose(graph: &Graph, budget: Budget) -> Result<TreeDecomposition, Error> {
vendor_size_guard(graph)?;
validate_budget(budget)?;
if graph.num_vertices == 0 {
return Ok(TreeDecomposition::from_parts(0, Vec::new(), Vec::new()));
}
native::run(graph, budget)
}
fn validate_budget(budget: Budget) -> Result<(), Error> {
match budget.kind {
BudgetKind::Timed {
timeout,
patience,
iterations,
steps,
..
} if timeout.is_zero()
|| patience.is_some_and(|patience| patience.is_zero())
|| iterations == 0
|| steps == 0 =>
{
return Err(Error::InvalidInput(
"a timed FlowCutter run needs positive time, steps, iterations, and patience when present"
.into(),
));
}
BudgetKind::Steps { steps, iterations } if steps == 0 || iterations == 0 => {
return Err(Error::InvalidInput(
"a step-budgeted FlowCutter run needs positive steps and iterations".into(),
));
}
_ => {}
}
let (steps, iterations) = match budget.kind {
BudgetKind::Timed {
timeout,
patience,
steps,
iterations,
..
} => {
if timeout.as_millis() > i64::MAX as u128
|| patience.is_some_and(|value| value.as_millis() > i64::MAX as u128)
{
return Err(Error::InvalidInput(
"FlowCutter duration does not fit in milliseconds".into(),
));
}
(steps, iterations)
}
BudgetKind::Steps { steps, iterations } => (steps, iterations),
};
if steps > i64::MAX as u64 {
return Err(Error::InvalidInput(
"FlowCutter step budget does not fit in i64".into(),
));
}
if iterations > i32::MAX as u32 {
return Err(Error::InvalidInput(
"FlowCutter iteration count does not fit in i32".into(),
));
}
Ok(())
}
#[cfg(test)]
mod tests;