use std::time::{Duration, Instant};
mod candidates;
mod config;
use crate::deadline::expired;
use crate::decomposition;
use crate::elimination::Order;
use crate::elimination::engine;
use crate::elimination::execution::ElimStop;
use crate::flowcutter::{Budget, decompose as flowcutter_decompose};
use crate::{Error, Graph, TreeDecomposition};
use candidates::{CandidateOutcome, CandidateSet};
use config::MIN_FLOWCUTTER_CANDIDATE_MS;
pub use config::PortfolioConfig;
const FLOWCUTTER_CANDIDATE_PATIENCE: Duration = Duration::from_millis(500);
const FLOWCUTTER_CANDIDATE_ITERATIONS: u32 = 50;
const SAMPLE_SEED_OFFSET: u64 = 100;
const SAMPLE_SEED_STRIDE: u64 = 7919;
pub(crate) const SECOND_CANDIDATE_SEED_OFFSET: u64 = 42;
const MAX_RESIDUAL_FOR_EXPENSIVE_ORDERS: usize = 10_000;
fn is_min_degree_variant(order: Order<'_>) -> bool {
matches!(order, Order::MinDegree | Order::MinDegreeSampled { .. })
}
fn flowcutter_candidate(
graph: &Graph,
configured_budget: Duration,
hard_deadline: Option<Instant>,
) -> Result<Option<TreeDecomposition>, Error> {
let timeout = hard_deadline
.map(crate::deadline::remaining)
.unwrap_or(configured_budget)
.min(configured_budget);
if timeout < Duration::from_millis(MIN_FLOWCUTTER_CANDIDATE_MS) {
return Ok(None);
}
match flowcutter_decompose(
graph,
Budget::timed(
timeout,
Some(FLOWCUTTER_CANDIDATE_PATIENCE),
FLOWCUTTER_CANDIDATE_ITERATIONS,
),
) {
Ok(decomposition) => Ok(Some(decomposition)),
Err(Error::NoDecomposition | Error::TooLarge(_)) => Ok(None),
Err(error) => Err(error),
}
}
type InitialOrderBuilder = for<'w> fn(u64, &'w [u32]) -> Vec<(Order<'w>, u64)>;
fn standard_orders(base_seed: u64, weights: &[u32]) -> Vec<(Order<'_>, u64)> {
let second_seed = base_seed.wrapping_add(SECOND_CANDIDATE_SEED_OFFSET);
vec![
(Order::MinDegreeSampled { weights }, base_seed),
(Order::NestedDissection, base_seed),
(Order::MinFillSampled { weights }, base_seed),
(Order::MinDegreeSampled { weights }, second_seed),
(Order::NestedDissection, second_seed),
]
}
fn sampled_min_fill_orders(base_seed: u64, weights: &[u32]) -> Vec<(Order<'_>, u64)> {
vec![(Order::MinFillSampled { weights }, base_seed)]
}
fn run_portfolio(
graph: &Graph,
weights: &[u32],
seed: u64,
initial_orders: InitialOrderBuilder,
config: PortfolioConfig,
) -> Result<Vec<TreeDecomposition>, crate::Error> {
config::validate(config)?;
let deadlines =
crate::deadline::two_stage(crate::meter::now(), config.soft_budget, "portfolio")?;
let soft_deadline = deadlines.soft;
let hard_deadline = deadlines.hard;
let mut prebuilt = engine::prebuild(graph);
let initial_orders = initial_orders(seed, weights);
let large_residual = prebuilt.num_active() > MAX_RESIDUAL_FOR_EXPENSIVE_ORDERS;
let mut candidates = CandidateSet::new(initial_orders.len() + 1);
let mut hard_deadline_tripped = false;
for (i, (order, candidate_seed)) in initial_orders.iter().copied().enumerate() {
if i > 0 && expired(soft_deadline) {
break;
}
if i > 0 && large_residual && !is_min_degree_variant(order) {
continue;
}
let complete_on_deadline = candidates.is_empty();
let run = engine::run_order_prebuilt(
&mut prebuilt,
engine::RunSpec {
order,
seed: candidate_seed,
stop: ElimStop {
soft_deadline,
hard_deadline,
width_bound: candidates.best_width(),
},
complete_on_deadline,
},
);
hard_deadline_tripped = match candidates.record_elimination(run) {
CandidateOutcome::DeadlineReached => true,
CandidateOutcome::WidthAborted => false,
CandidateOutcome::Produced => expired(hard_deadline),
};
if hard_deadline_tripped {
break;
}
}
let sample_order = if large_residual {
Order::MinDegreeSampled { weights }
} else {
Order::MinFillSampled { weights }
};
let max_samples = config.sampling_runs;
let mut sample_index: u64 = 0;
while sample_index < max_samples
&& !hard_deadline_tripped
&& !expired(soft_deadline)
&& !expired(hard_deadline)
{
let sample_seed =
seed.wrapping_add(SAMPLE_SEED_OFFSET + sample_index.wrapping_mul(SAMPLE_SEED_STRIDE));
let run = engine::run_order_prebuilt(
&mut prebuilt,
engine::RunSpec {
order: sample_order,
seed: sample_seed,
stop: ElimStop {
soft_deadline,
hard_deadline,
width_bound: candidates.best_width(),
},
complete_on_deadline: false,
},
);
match candidates.record_elimination(run) {
CandidateOutcome::DeadlineReached => break,
CandidateOutcome::Produced | CandidateOutcome::WidthAborted => sample_index += 1,
}
}
if let Some(configured_budget) = config
.flowcutter_budget
.filter(|_| !hard_deadline_tripped && !expired(hard_deadline))
&& let Some(decomposition) = flowcutter_candidate(graph, configured_budget, hard_deadline)?
{
candidates.push(decomposition);
}
Ok(candidates.into_decompositions())
}
pub fn sampled_min_fill_candidates(
graph: &Graph,
weights: &[u32],
seed: u64,
config: PortfolioConfig,
) -> Result<Vec<TreeDecomposition>, crate::Error> {
validate_weights(graph, weights)?;
run_portfolio(graph, weights, seed, sampled_min_fill_orders, config)
}
pub fn candidates(
graph: &Graph,
weights: &[u32],
seed: u64,
config: PortfolioConfig,
) -> Result<Vec<TreeDecomposition>, crate::Error> {
validate_weights(graph, weights)?;
let mut decompositions = run_portfolio(graph, weights, seed, standard_orders, config)?;
decompositions.sort_by_key(TreeDecomposition::quality_key);
Ok(decompositions)
}
pub fn decompose(
graph: &Graph,
weights: &[u32],
seed: u64,
config: PortfolioConfig,
) -> Result<TreeDecomposition, crate::Error> {
Ok(candidates(graph, weights, seed, config)?
.into_iter()
.next()
.expect("first candidate always produces a decomposition"))
}
pub fn decompose_and_refine(
graph: &Graph,
weights: &[u32],
seed: u64,
config: PortfolioConfig,
refinement_budget: Option<Duration>,
) -> Result<TreeDecomposition, crate::Error> {
let td = decompose(graph, weights, seed, config)?;
decomposition::refine_with_flowcutter(td, graph, refinement_budget)
}
fn validate_weights(graph: &Graph, weights: &[u32]) -> Result<(), crate::Error> {
if weights.len() != graph.num_vertices as usize {
return Err(crate::Error::InvalidInput(format!(
"portfolio has {} weights for {} vertices",
weights.len(),
graph.num_vertices
)));
}
Ok(())
}