use super::csr::CsrGraph;
use crate::partition::common::{FmBalance, GainBuckets, Stall, commit_best_prefix, fm_balance};
pub(super) struct FmScratch {
gain: Vec<i64>,
cut_edges: Vec<i64>,
locked: Vec<bool>,
moves: Vec<usize>,
cumulative_gain: Vec<i64>,
bq: [GainBuckets; 2],
}
impl FmScratch {
pub(super) fn new() -> Self {
FmScratch {
gain: Vec::new(),
cut_edges: Vec::new(),
locked: Vec::new(),
moves: Vec::new(),
cumulative_gain: Vec::new(),
bq: [GainBuckets::empty(), GainBuckets::empty()],
}
}
fn prepare(&mut self, n: usize) {
self.gain.clear();
self.gain.resize(n, 0);
self.cut_edges.clear();
self.cut_edges.resize(n, 0);
self.locked.clear();
self.locked.resize(n, false);
self.moves.clear();
self.cumulative_gain.clear();
self.bq[0].reset(n);
self.bq[1].reset(n);
}
}
pub(super) fn fm_refine_pass(
graph: &CsrGraph,
part: &mut [u8],
max_imbalance: f64,
scratch: &mut FmScratch,
) -> bool {
crate::meter::charge(graph.pass_units());
let n = graph.num_vertices();
let Some(FmBalance {
mut weight,
min_part_weight,
max_part_weight,
}) = fm_balance(n, &graph.vertex_weights, part, max_imbalance)
else {
return false;
};
scratch.prepare(n);
let gain = scratch.gain.as_mut_slice();
let cut_edges = scratch.cut_edges.as_mut_slice();
for v in 0..n {
let my_part = part[v];
let start = graph.offsets[v] as usize;
let end = graph.offsets[v + 1] as usize;
let nbrs = &graph.neighbors[start..end];
let weights = &graph.edge_weights[start..end];
let mut g = 0i64;
let mut cut = 0i64;
for (&nb, &w) in nbrs.iter().zip(weights) {
let w = i64::from(w);
if part[nb as usize] != my_part {
g += w;
cut += w;
} else {
g -= w;
}
}
gain[v] = g;
cut_edges[v] = cut;
}
let bq = &mut scratch.bq;
for v in 0..n {
if cut_edges[v] > 0 {
bq[part[v] as usize].insert(v, gain[v]);
}
}
let locked = scratch.locked.as_mut_slice();
let moves = &mut scratch.moves;
let cumulative_gain = &mut scratch.cumulative_gain;
let mut running_gain: i64 = 0;
let mut stall = Stall::new((n / 2).max(20));
for _ in 0..n {
let mut best_v: Option<usize> = None;
let mut best_gain = i64::MIN;
let mut best_from: usize = 0;
for side in 0..2 {
let candidate = bq[side].best_satisfying(|vertex| {
let to = 1 - side;
!locked[vertex]
&& weight[side] - graph.vertex_weights[vertex] >= min_part_weight
&& weight[to] + graph.vertex_weights[vertex] <= max_part_weight
});
if let Some(vertex) = candidate {
let g = gain[vertex];
if g > best_gain {
best_gain = g;
best_v = Some(vertex);
best_from = side;
}
}
}
let v = match best_v {
Some(v) => v,
None => break,
};
let from = best_from;
let to = 1 - from;
bq[from].remove(v);
weight[from] -= graph.vertex_weights[v];
weight[to] += graph.vertex_weights[v];
part[v] = to as u8;
locked[v] = true;
running_gain += best_gain;
moves.push(v);
cumulative_gain.push(running_gain);
if stall.record(running_gain) {
break;
}
let v_start = graph.offsets[v] as usize;
let v_end = graph.offsets[v + 1] as usize;
let v_nbrs = &graph.neighbors[v_start..v_end];
let v_weights = &graph.edge_weights[v_start..v_end];
for (&nb_raw, &w_raw) in v_nbrs.iter().zip(v_weights) {
let nb = nb_raw as usize;
if locked[nb] {
continue;
}
let w = i64::from(w_raw);
let nb_part = part[nb] as usize;
let was_in_queue = bq[nb_part].contains(nb);
if nb_part == to {
gain[nb] -= 2 * w;
cut_edges[nb] -= w;
} else {
gain[nb] += 2 * w;
cut_edges[nb] += w;
}
let on_boundary = cut_edges[nb] > 0;
if on_boundary {
if was_in_queue {
bq[nb_part].update(nb, gain[nb]);
} else {
bq[nb_part].insert(nb, gain[nb]);
}
} else if was_in_queue {
bq[nb_part].remove(nb);
}
}
}
commit_best_prefix(moves, cumulative_gain, part)
}
pub(super) fn localized_fm_pass(
graph: &CsrGraph,
part: &mut [u8],
seed: usize,
max_imbalance: f64,
) -> bool {
let n = graph.num_vertices();
let Some(FmBalance {
mut weight,
min_part_weight,
max_part_weight,
}) = fm_balance(n, &graph.vertex_weights, part, max_imbalance)
else {
return false;
};
let max_region = (n / 4).max(20).min(n);
let mut in_region = vec![false; n];
let mut region_list: Vec<usize> = Vec::with_capacity(max_region);
let mut queue = std::collections::VecDeque::new();
in_region[seed] = true;
queue.push_back(seed);
region_list.push(seed);
while let Some(v) = queue.pop_front() {
if region_list.len() >= max_region {
break;
}
for &nb in graph.neighbors(v) {
let nb = nb as usize;
if !in_region[nb] && part[nb] != part[v] && region_list.len() < max_region {
in_region[nb] = true;
queue.push_back(nb);
region_list.push(nb);
}
}
for &nb in graph.neighbors(v) {
let nb = nb as usize;
if !in_region[nb] && region_list.len() < max_region {
let nb_part = part[nb];
let is_boundary = graph
.neighbors(nb)
.iter()
.any(|&nnb| part[nnb as usize] != nb_part);
if is_boundary {
in_region[nb] = true;
queue.push_back(nb);
region_list.push(nb);
}
}
}
}
let mut gain = vec![0i64; n];
for &v in ®ion_list {
let my_part = part[v];
let start = graph.offsets[v] as usize;
let end = graph.offsets[v + 1] as usize;
let nbrs = &graph.neighbors[start..end];
let weights = &graph.edge_weights[start..end];
let mut g = 0i64;
for (&nb, &w) in nbrs.iter().zip(weights) {
let w = i64::from(w);
if part[nb as usize] != my_part {
g += w;
} else {
g -= w;
}
}
gain[v] = g;
}
let mut locked = vec![false; n];
let mut moves: Vec<usize> = Vec::new();
let mut cumulative_gain: Vec<i64> = Vec::new();
let mut running_gain: i64 = 0;
let mut stall = Stall::new(region_list.len() / 2);
for _ in 0..region_list.len() {
let mut best_v = None;
let mut best_g = i64::MIN;
for &v in ®ion_list {
if locked[v] {
continue;
}
let from = part[v] as usize;
let to = 1 - from;
let nfw = weight[from] - graph.vertex_weights[v];
let ntw = weight[to] + graph.vertex_weights[v];
if nfw < min_part_weight || ntw > max_part_weight {
continue;
}
if best_v.is_none() || gain[v] > best_g {
best_g = gain[v];
best_v = Some(v);
}
}
let Some(v) = best_v else {
break;
};
let from = part[v] as usize;
let to = 1 - from;
weight[from] -= graph.vertex_weights[v];
weight[to] += graph.vertex_weights[v];
part[v] = to as u8;
locked[v] = true;
running_gain += best_g;
moves.push(v);
cumulative_gain.push(running_gain);
if stall.record(running_gain) {
break;
}
let v_start = graph.offsets[v] as usize;
let v_end = graph.offsets[v + 1] as usize;
let v_nbrs = &graph.neighbors[v_start..v_end];
let v_weights = &graph.edge_weights[v_start..v_end];
for (&nb_raw, &w_raw) in v_nbrs.iter().zip(v_weights) {
let nb = nb_raw as usize;
if locked[nb] || !in_region[nb] {
continue;
}
let w = i64::from(w_raw);
if part[nb] == to as u8 {
gain[nb] -= 2 * w;
} else {
gain[nb] += 2 * w;
}
}
}
commit_best_prefix(&moves, &cumulative_gain, part)
}
pub(super) fn refine_level(
graph: &CsrGraph,
part: &mut [u8],
max_imbalance: f64,
scratch: &mut FmScratch,
) {
let max_passes = 10;
for _ in 0..max_passes {
if !fm_refine_pass(graph, part, max_imbalance, scratch) {
break;
}
}
}
pub(super) fn refine_finest_level(
graph: &CsrGraph,
part: &mut [u8],
max_imbalance: f64,
scratch: &mut FmScratch,
) {
refine_level(graph, part, max_imbalance, scratch);
let n = graph.num_vertices();
if n < 20 {
return;
}
let num_tries = 4.min(n);
let mut boundary: Vec<usize> = Vec::new();
for v in 0..n {
let my_part = part[v];
if graph
.neighbors(v)
.iter()
.any(|&nb| part[nb as usize] != my_part)
{
boundary.push(v);
}
}
if boundary.is_empty() {
return;
}
for i in 0..num_tries {
let seed = boundary[(i * 7919) % boundary.len()];
localized_fm_pass(graph, part, seed, max_imbalance);
}
}