use rand::rngs::StdRng;
use rand::seq::SliceRandom;
use rand::{Rng, SeedableRng};
use crate::core::math::sparse::CompressedSparseData2;
use super::params::PartitionParams;
const EPSILON: f64 = 1e-6;
const INNER_SLOPE: f64 = 0.02;
const OUTER_SLOPE: f64 = 0.5;
#[derive(Clone, Copy, Debug)]
struct NodeScore {
out_weight: f64,
in_weight: f64,
score: f64,
}
impl NodeScore {
fn new() -> Self {
Self {
out_weight: 0.0,
in_weight: 0.0,
score: (EPSILON.log2()) / 2.0,
}
}
#[inline]
fn update_outgoing(&mut self, direction: i32, w: f64) {
self.out_weight += direction as f64 * w;
if self.out_weight < 0.0 {
self.out_weight = 0.0;
}
}
#[inline]
fn update_incoming(&mut self, direction: i32, w: f64) {
self.in_weight += direction as f64 * w;
if self.in_weight < 0.0 {
self.in_weight = 0.0;
}
}
#[inline]
fn connectivity(&self) -> i32 {
let out = if self.out_weight > EPSILON { 1 } else { 0 };
let inc = if self.in_weight > 0.0 { 1 } else { 0 };
out + inc
}
#[inline]
fn rescore(&mut self) -> f64 {
self.score = (EPSILON + self.out_weight * self.in_weight).log2() / 2.0;
self.score
}
}
fn mass_factor(mass: f64, low: f64, target: f64, high: f64) -> f64 {
if mass < low {
((1.0 - INNER_SLOPE) * low / (low + (1.0 / (1.0 - OUTER_SLOPE) - 1.0) * (low - mass)))
.log2()
} else if mass < target {
(1.0 - INNER_SLOPE * (target - mass) / (target - low)).log2()
} else if mass < high {
(1.0 - INNER_SLOPE * (mass - target) / (high - target)).log2()
} else {
((1.0 - INNER_SLOPE) * low / (low + (1.0 / (1.0 - OUTER_SLOPE) - 1.0) * (mass - high)))
.log2()
}
}
fn initial_incoming_scale(incoming: &CompressedSparseData2<f32, f32>) -> f64 {
let n = incoming.shape.0;
let mut acc = 0.0f64;
for i in 0..n {
let s: f64 = (incoming.indptr[i]..incoming.indptr[i + 1])
.map(|idx| incoming.data[idx as usize] as f64)
.sum();
if s > 0.0 {
acc += s.log2();
}
}
acc
}
fn initial_size_of_partitions(partition_of_nodes: &[i32]) -> Vec<usize> {
let n_partitions = partition_of_nodes
.iter()
.copied()
.max()
.unwrap_or(-1)
.max(0) as usize
+ 1;
let mut sizes = vec![0usize; n_partitions];
for &p in partition_of_nodes {
if p >= 0 {
sizes[p as usize] += 1;
}
}
sizes
}
fn initial_mass_of_partitions(
partition_of_nodes: &[i32],
mass_of_nodes: &[f32],
n_partitions: usize,
) -> Vec<f64> {
let mut masses = vec![0.0f64; n_partitions];
for (i, &p) in partition_of_nodes.iter().enumerate() {
if p >= 0 {
masses[p as usize] += mass_of_nodes[i] as f64;
}
}
masses
}
fn initial_score_table(
outgoing: &CompressedSparseData2<f32, f32>,
partition_of_nodes: &[i32],
n_partitions: usize,
) -> Vec<Vec<NodeScore>> {
let n = outgoing.shape.0;
let mut table: Vec<Vec<NodeScore>> = (0..n_partitions)
.map(|_| vec![NodeScore::new(); n])
.collect();
for src in 0..n {
let src_p = partition_of_nodes[src];
let start = outgoing.indptr[src] as usize;
let end = outgoing.indptr[src + 1] as usize;
for idx in start..end {
let dst = outgoing.indices[idx] as usize;
let w = outgoing.data[idx] as f64;
let dst_p = partition_of_nodes[dst];
if dst_p >= 0 {
let s = &mut table[dst_p as usize][src];
s.update_outgoing(1, w);
s.rescore();
}
if src_p >= 0 {
let s = &mut table[src_p as usize][dst];
s.update_incoming(1, w);
s.rescore();
}
}
}
table
}
fn initial_score_of_partitions(
partition_of_nodes: &[i32],
table: &[Vec<NodeScore>],
n_partitions: usize,
) -> Vec<f64> {
let mut scores = vec![0.0f64; n_partitions];
for (i, &p) in partition_of_nodes.iter().enumerate() {
if p >= 0 {
scores[p as usize] += table[p as usize][i].score;
}
}
scores
}
struct OptimizePartitions<'a> {
outgoing: &'a CompressedSparseData2<f32, f32>,
incoming: &'a CompressedSparseData2<f32, f32>,
n: usize,
mass_of_nodes: &'a [f32],
low_mass: f64,
target_mass: f64,
high_mass: f64,
low_size: f64,
target_size: f64,
high_size: f64,
partition_of_nodes: &'a mut [i32],
incoming_scale: f64,
table: Vec<Vec<NodeScore>>,
size_of_partitions: Vec<usize>,
mass_of_partitions: Vec<f64>,
score_of_partitions: Vec<f64>,
n_partitions: usize,
hot_partitions: Vec<bool>,
temperature_of_nodes: Vec<f64>,
}
impl<'a> OptimizePartitions<'a> {
#[allow(clippy::too_many_arguments)]
fn new(
outgoing: &'a CompressedSparseData2<f32, f32>,
incoming: &'a CompressedSparseData2<f32, f32>,
mass_of_nodes: &'a [f32],
low_mass: f64,
target_mass: f64,
high_mass: f64,
low_size: f64,
target_size: f64,
high_size: f64,
partition_of_nodes: &'a mut [i32],
hot_partitions: Vec<bool>,
) -> Self {
let n = outgoing.shape.0;
assert_eq!(incoming.shape.0, n);
assert_eq!(partition_of_nodes.len(), n);
assert_eq!(mass_of_nodes.len(), n);
assert!(0.0 < low_mass && low_mass < target_mass && target_mass < high_mass);
assert!(0.0 < low_size && low_size < target_size && target_size < high_size);
let size_of_partitions = initial_size_of_partitions(partition_of_nodes);
let n_partitions = size_of_partitions.len();
let incoming_scale = initial_incoming_scale(incoming);
let table = initial_score_table(outgoing, partition_of_nodes, n_partitions);
let mass_of_partitions =
initial_mass_of_partitions(partition_of_nodes, mass_of_nodes, n_partitions);
let score_of_partitions =
initial_score_of_partitions(partition_of_nodes, &table, n_partitions);
let mut hot = hot_partitions;
if hot.len() < n_partitions {
hot.resize(n_partitions, false);
}
Self {
outgoing,
incoming,
n,
mass_of_nodes,
low_mass,
target_mass,
high_mass,
low_size,
target_size,
high_size,
partition_of_nodes,
incoming_scale,
table,
size_of_partitions,
mass_of_partitions,
score_of_partitions,
n_partitions,
hot_partitions: hot,
temperature_of_nodes: vec![1.0; n],
}
}
fn score(&self, with_orphans: bool) -> f64 {
let mut total = (self.n as f64) * (self.n as f64).log2() - self.incoming_scale;
let mut orphans = self.n;
for p in 0..self.n_partitions {
let s = self.score_of_partitions[p];
let m = self.mass_of_partitions[p];
let sz = self.size_of_partitions[p];
if sz > 0 {
total += s - (sz as f64) * (sz as f64).log2();
let mf = mass_factor(m, self.low_mass, self.target_mass, self.high_mass);
let sf = mass_factor(sz as f64, self.low_size, self.target_size, self.high_size);
total += (sz as f64) * mf.min(sf);
}
orphans -= sz;
}
if with_orphans {
total += (orphans as f64) * NodeScore::new().score;
total / self.n as f64
} else {
total / (self.n - orphans) as f64
}
}
fn optimise(
&mut self,
random_seed: u64,
cooldown_pass: f64,
cooldown_node: f64,
cold_temperature: f64,
) {
assert!((0.0..=1.0).contains(&cooldown_node));
if cooldown_pass != 0.0 {
assert!(cooldown_pass > 0.0 && cooldown_pass < 1.0);
}
let mut rng = StdRng::seed_from_u64(random_seed);
let mut frozen = vec![false; self.n];
for i in 0..self.n {
let p = self.partition_of_nodes[i];
if (0..self.n_partitions as i32).contains(&p) && !self.hot_partitions[p as usize] {
self.temperature_of_nodes[i] = cold_temperature;
frozen[i] = true;
} else if p < -1 {
frozen[i] = true;
} else {
self.temperature_of_nodes[i] = 1.0;
}
}
let cooldown_rate = 1.0 - cooldown_pass / self.n as f64;
let mut temperature = 1.0_f64;
let mut indices: Vec<usize> = (0..self.n).collect();
let mut did_improve = true;
let mut did_skip = false;
while temperature > 0.0 || did_improve {
if did_improve {
did_improve = false;
did_skip = false;
} else if did_skip {
did_skip = false;
let mut next_t = temperature;
for i in 0..self.n {
if !frozen[i] && self.temperature_of_nodes[i] < next_t {
next_t = self.temperature_of_nodes[i];
}
}
temperature = next_t;
} else {
temperature = 0.0;
}
indices.shuffle(&mut rng);
for &i in &indices {
temperature *= cooldown_rate;
if self.temperature_of_nodes[i] < temperature {
did_skip = true;
continue;
}
if self.improve_node(i, &mut rng, temperature) {
frozen[i] = false;
did_improve = true;
} else {
frozen[i] = false;
self.temperature_of_nodes[i] = temperature * (1.0 - cooldown_node);
}
}
}
}
fn improve_node(&mut self, node_index: usize, rng: &mut StdRng, temperature: f64) -> bool {
let current_p = self.partition_of_nodes[node_index];
if current_p < 0 || self.size_of_partitions[current_p as usize] < 2 {
return false;
}
let current_p = current_p as usize;
let mut cold_diffs = vec![0.0f64; self.n_partitions];
let mut hot_diffs = vec![0.0f64; self.n_partitions];
let mut conn_diffs = vec![0.0f64; self.n_partitions];
let mut disc_diffs = vec![0.0f64; self.n_partitions];
self.collect_initial_diffs(node_index, current_p, &mut cold_diffs, &mut hot_diffs);
let current_conn = self.table[current_p][node_index].connectivity();
self.collect_cold_diffs(
node_index,
current_p,
current_conn,
&mut cold_diffs,
&mut conn_diffs,
&mut disc_diffs,
);
let (current_cold_diff, total_diffs, candidates) = self.collect_candidates(
node_index,
current_p,
current_conn,
&cold_diffs,
&hot_diffs,
&conn_diffs,
&disc_diffs,
temperature,
);
let chosen = self.choose_target(&candidates, &total_diffs, rng);
let Some(chosen_p) = chosen else {
return false;
};
let chosen_cold_diff = cold_diffs[chosen_p];
self.apply_move(
node_index,
current_p,
chosen_p,
current_cold_diff,
chosen_cold_diff,
);
true
}
fn collect_initial_diffs(
&self,
node_index: usize,
current_p: usize,
cold: &mut [f64],
hot: &mut [f64],
) {
for p in 0..self.n_partitions {
let direction = if p == current_p { -1.0 } else { 1.0 };
let s = direction * self.table[p][node_index].score;
cold[p] = s;
hot[p] = s;
}
}
fn collect_cold_diffs(
&self,
node_index: usize,
current_p: usize,
current_conn: i32,
cold: &mut [f64],
conn: &mut [f64],
disc: &mut [f64],
) {
for p in 0..self.n_partitions {
if p == current_p {
conn[p] = 0.0;
disc[p] = 0.0;
continue;
}
let other_conn = self.table[p][node_index].connectivity();
conn[p] = (other_conn - current_conn) as f64;
disc[p] = if current_conn > 0 && other_conn == 0 {
1.0
} else if current_conn == 0 && other_conn > 0 {
-1.0
} else {
0.0
};
}
let out_start = self.outgoing.indptr[node_index] as usize;
let out_end = self.outgoing.indptr[node_index + 1] as usize;
let in_start = self.incoming.indptr[node_index] as usize;
let in_end = self.incoming.indptr[node_index + 1] as usize;
let mut op = out_start;
let mut ip = in_start;
while op < out_end || ip < in_end {
let out_node = if op < out_end {
self.outgoing.indices[op] as i64
} else {
self.n as i64
};
let in_node = if ip < in_end {
self.incoming.indices[ip] as i64
} else {
self.n as i64
};
let other = out_node.min(in_node) as usize;
let is_out = out_node == other as i64;
let is_in = in_node == other as i64;
let out_w = if is_out {
self.outgoing.data[op] as f64
} else {
0.0
};
let in_w = if is_in {
self.incoming.data[ip] as f64
} else {
0.0
};
let other_p = self.partition_of_nodes[other];
if other_p >= 0 {
let other_p = other_p as usize;
let direction: i32 = if other_p == current_p { -1 } else { 1 };
let mut s = self.table[other_p][other];
let old_score = s.score;
let old_conn = s.connectivity();
if is_out {
s.update_incoming(direction * (is_out as i32), out_w);
}
if is_in {
s.update_outgoing(direction * (is_in as i32), in_w);
}
let new_score = s.rescore();
let new_conn = s.connectivity();
cold[other_p] += new_score - old_score;
conn[other_p] += (new_conn - old_conn) as f64;
if old_conn == 0 && new_conn > 0 {
disc[other_p] -= 1.0;
}
if old_conn > 0 && new_conn == 0 {
disc[other_p] += 1.0;
}
}
if is_out {
op += 1;
}
if is_in {
ip += 1;
}
}
}
#[allow(clippy::too_many_arguments)]
fn collect_candidates(
&self,
node_index: usize,
current_p: usize,
current_conn: i32,
cold: &[f64],
hot: &[f64],
conn: &[f64],
disc: &[f64],
temperature: f64,
) -> (f64, Vec<f64>, Candidates) {
let node_mass = self.mass_of_nodes[node_index] as f64;
let current_hot_diff = hot[current_p];
let current_cold_diff = cold[current_p];
let min_conn_diff = if current_conn == 0 { 1 } else { 0 };
let old_size = self.size_of_partitions[current_p];
let old_mass = self.mass_of_partitions[current_p];
let old_mf = mass_factor(old_mass, self.low_mass, self.target_mass, self.high_mass);
let old_sf = mass_factor(
old_size as f64,
self.low_size,
self.target_size,
self.high_size,
);
let old_score = self.score_of_partitions[current_p];
let old_adj = old_score - (old_size as f64) * (old_size as f64).log2()
+ (old_size as f64) * old_mf.min(old_sf);
let new_size = old_size - 1;
let new_mass = old_mass - node_mass;
let new_mf = mass_factor(new_mass, self.low_mass, self.target_mass, self.high_mass);
let new_sf = mass_factor(
new_size as f64,
self.low_size,
self.target_size,
self.high_size,
);
let new_score = old_score + current_cold_diff;
let new_adj = if new_size > 0 {
new_score - (new_size as f64) * (new_size as f64).log2()
+ (new_size as f64) * new_mf.min(new_sf)
} else {
0.0
};
let current_adj_cold_diff = new_adj - old_adj;
let current_conn_diff = conn[current_p];
let current_disc_diff = disc[current_p];
let mut total_diffs = vec![0.0f64; self.n_partitions];
let mut cands = Candidates::default();
for p in 0..self.n_partitions {
if p == current_p {
total_diffs[p] = 0.0;
continue;
}
let old_size = self.size_of_partitions[p];
let old_mass = self.mass_of_partitions[p];
let old_mf = mass_factor(old_mass, self.low_mass, self.target_mass, self.high_mass);
let old_sf = mass_factor(
old_size as f64,
self.low_size,
self.target_size,
self.high_size,
);
let old_score = self.score_of_partitions[p];
let old_adj = if old_size > 0 {
old_score - (old_size as f64) * (old_size as f64).log2()
+ (old_size as f64) * old_mf.min(old_sf)
} else {
0.0
};
let new_size = old_size + 1;
let new_mass = old_mass + node_mass;
let new_mf = mass_factor(new_mass, self.low_mass, self.target_mass, self.high_mass);
let new_sf = mass_factor(
new_size as f64,
self.low_size,
self.target_size,
self.high_size,
);
let new_score = old_score + cold[p];
let new_adj = new_score - (new_size as f64) * (new_size as f64).log2()
+ (new_size as f64) * new_mf.min(new_sf);
let p_conn_diff = conn[p];
let adjusted_conn_diff = p_conn_diff + current_conn_diff;
let p_disc_diff = disc[p];
if p_disc_diff + current_disc_diff > 0.0 {
continue;
}
if (adjusted_conn_diff as i32) < min_conn_diff {
continue;
}
let adj_cold_diff = new_adj - old_adj;
let total = (current_hot_diff + hot[p]) * temperature
+ (current_adj_cold_diff + adj_cold_diff) * (1.0 - temperature);
total_diffs[p] = total;
if total >= EPSILON {
if min_conn_diff > 0 {
cands.both.push(p);
} else {
cands.goal.push(p);
}
} else if min_conn_diff > 0 {
cands.connectivity.push(p);
}
}
(current_cold_diff, total_diffs, cands)
}
fn choose_target(
&self,
candidates: &Candidates,
total_diffs: &[f64],
rng: &mut StdRng,
) -> Option<usize> {
let mut pick = |list: &[usize]| -> Option<usize> {
if list.is_empty() {
return None;
}
let min_total = list
.iter()
.map(|&p| total_diffs[p])
.fold(f64::INFINITY, f64::min);
let sum: f64 = list.iter().map(|&p| total_diffs[p] - min_total).sum();
if sum <= 0.0 {
return Some(list[rng.random_range(0..list.len())]);
}
let mut roll: f64 = rng.random::<f64>() * sum;
for &p in list {
roll -= total_diffs[p] - min_total;
if roll <= EPSILON {
return Some(p);
}
}
Some(list[list.len() - 1]) };
pick(&candidates.both)
.or_else(|| pick(&candidates.connectivity))
.or_else(|| pick(&candidates.goal))
}
fn apply_move(
&mut self,
node_index: usize,
from_p: usize,
to_p: usize,
from_cold_diff: f64,
to_cold_diff: f64,
) {
let out_start = self.outgoing.indptr[node_index] as usize;
let out_end = self.outgoing.indptr[node_index + 1] as usize;
let in_start = self.incoming.indptr[node_index] as usize;
let in_end = self.incoming.indptr[node_index + 1] as usize;
let mut op = out_start;
let mut ip = in_start;
while op < out_end || ip < in_end {
let out_node = if op < out_end {
self.outgoing.indices[op] as i64
} else {
self.n as i64
};
let in_node = if ip < in_end {
self.incoming.indices[ip] as i64
} else {
self.n as i64
};
let other = out_node.min(in_node) as usize;
let is_out = out_node == other as i64;
let is_in = in_node == other as i64;
let out_w = if is_out {
self.outgoing.data[op] as f64
} else {
0.0
};
let in_w = if is_in {
self.incoming.data[ip] as f64
} else {
0.0
};
let s_from = &mut self.table[from_p][other];
if is_out {
s_from.update_incoming(-1, out_w);
}
if is_in {
s_from.update_outgoing(-1, in_w);
}
s_from.rescore();
let s_to = &mut self.table[to_p][other];
if is_out {
s_to.update_incoming(1, out_w);
}
if is_in {
s_to.update_outgoing(1, in_w);
}
s_to.rescore();
if is_out {
op += 1;
}
if is_in {
ip += 1;
}
}
debug_assert_eq!(self.partition_of_nodes[node_index], from_p as i32);
self.partition_of_nodes[node_index] = to_p as i32;
self.size_of_partitions[from_p] -= 1;
self.size_of_partitions[to_p] += 1;
let m = self.mass_of_nodes[node_index] as f64;
self.mass_of_partitions[from_p] -= m;
self.mass_of_partitions[to_p] += m;
self.score_of_partitions[from_p] += from_cold_diff;
self.score_of_partitions[to_p] += to_cold_diff;
}
}
#[derive(Default)]
struct Candidates {
both: Vec<usize>,
connectivity: Vec<usize>,
goal: Vec<usize>,
}
#[allow(clippy::too_many_arguments)]
pub fn optimise_partitions(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
mass_of_nodes: &[f32],
partition_of_nodes: &mut [i32],
low_mass: f64,
target_mass: f64,
high_mass: f64,
low_size: f64,
target_size: f64,
high_size: f64,
params: &PartitionParams,
cold_temperature: f64,
hot_partitions: Vec<bool>,
random_seed: u64,
) -> f64 {
let mut opt = OptimizePartitions::new(
outgoing,
incoming,
mass_of_nodes,
low_mass,
target_mass,
high_mass,
low_size,
target_size,
high_size,
partition_of_nodes,
hot_partitions,
);
opt.optimise(
random_seed,
params.cooldown_pass,
params.cooldown_node,
cold_temperature,
);
opt.score(true)
}
#[allow(clippy::too_many_arguments)]
pub fn score_partitions(
outgoing: &CompressedSparseData2<f32, f32>,
incoming: &CompressedSparseData2<f32, f32>,
mass_of_nodes: &[f32],
partition_of_nodes: &mut [i32],
low_mass: f64,
target_mass: f64,
high_mass: f64,
low_size: f64,
target_size: f64,
high_size: f64,
with_orphans: bool,
) -> f64 {
let n_partitions = (partition_of_nodes
.iter()
.copied()
.max()
.unwrap_or(-1)
.max(0)
+ 1) as usize;
let opt = OptimizePartitions::new(
outgoing,
incoming,
mass_of_nodes,
low_mass,
target_mass,
high_mass,
low_size,
target_size,
high_size,
partition_of_nodes,
vec![false; n_partitions],
);
opt.score(with_orphans)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{core::math::sparse::CompressedSparseFormat, prelude::VecIndexCast};
fn make_graph(
edges: &[(usize, usize, f32)],
n: usize,
) -> (
CompressedSparseData2<f32, f32>,
CompressedSparseData2<f32, f32>,
) {
let mut sorted: Vec<(usize, usize, f32)> = edges.to_vec();
sorted.sort_unstable_by_key(|a| (a.0, a.1));
let mut data = Vec::new();
let mut indices = Vec::new();
let mut indptr = vec![0usize; n + 1];
for &(r, c, v) in &sorted {
data.push(v);
indices.push(c);
indptr[r + 1] += 1;
}
for i in 0..n {
indptr[i + 1] += indptr[i];
}
let outgoing = CompressedSparseData2 {
data,
indices: indices.index_cast(),
indptr: indptr.index_cast(),
cs_type: CompressedSparseFormat::Csr,
data_2: None,
shape: (n, n),
};
let inc = crate::core::math::sparse::transpose_sparse(&outgoing);
let incoming = CompressedSparseData2 {
data: inc.data,
indices: inc.indices,
indptr: inc.indptr,
cs_type: CompressedSparseFormat::Csr,
data_2: inc.data_2,
shape: inc.shape,
};
(outgoing, incoming)
}
#[test]
fn mass_factor_is_zero_at_target() {
let mf = mass_factor(100.0, 50.0, 100.0, 200.0);
assert!(mf.abs() < 1e-9, "expected 0 at target, got {}", mf);
}
#[test]
fn mass_factor_is_negative_off_target() {
let inside_lo = mass_factor(75.0, 50.0, 100.0, 200.0);
let inside_hi = mass_factor(150.0, 50.0, 100.0, 200.0);
let outside_lo = mass_factor(20.0, 50.0, 100.0, 200.0);
let outside_hi = mass_factor(400.0, 50.0, 100.0, 200.0);
assert!(inside_lo < 0.0 && inside_lo > -0.1);
assert!(inside_hi < 0.0 && inside_hi > -0.1);
assert!(outside_lo < inside_lo, "outside-low must be sharper");
assert!(outside_hi < inside_hi, "outside-high must be sharper");
}
#[test]
fn node_score_default_is_log_epsilon_half() {
let s = NodeScore::new();
let expected = EPSILON.log2() / 2.0;
assert!((s.score - expected).abs() < 1e-12);
assert_eq!(s.connectivity(), 0);
}
#[test]
fn two_clique_graph_separates_correctly() {
let edges = vec![
(0, 1, 1.0),
(1, 0, 1.0),
(1, 2, 1.0),
(2, 1, 1.0),
(0, 2, 1.0),
(2, 0, 1.0),
(3, 4, 1.0),
(4, 3, 1.0),
(4, 5, 1.0),
(5, 4, 1.0),
(3, 5, 1.0),
(5, 3, 1.0),
(0, 3, 0.01),
(3, 0, 0.01),
];
let (out, inc) = make_graph(&edges, 6);
let mass = vec![1.0f32; 6];
let mut assignment = vec![0i32, 1, 0, 1, 0, 1];
let params = PartitionParams {
cooldown_pass: 0.05,
cooldown_node: 0.25,
..PartitionParams::default()
};
let _score = optimise_partitions(
&out,
&inc,
&mass,
&mut assignment,
0.5,
3.0,
10.0,
0.5,
3.0,
10.0,
¶ms,
0.05,
vec![true, true],
42,
);
assert_eq!(assignment[0], assignment[1], "0 and 1 in same clique");
assert_eq!(assignment[1], assignment[2], "1 and 2 in same clique");
assert_eq!(assignment[3], assignment[4], "3 and 4 in same clique");
assert_eq!(assignment[4], assignment[5], "4 and 5 in same clique");
assert_ne!(
assignment[0], assignment[3],
"cliques must end up separated"
);
}
#[test]
fn score_is_finite_on_trivial_input() {
let edges = vec![(0, 1, 1.0), (1, 0, 1.0)];
let (out, inc) = make_graph(&edges, 2);
let mass = vec![1.0f32; 2];
let mut assignment = vec![0i32, 1];
let s = score_partitions(
&out,
&inc,
&mass,
&mut assignment,
0.5,
1.0,
2.0,
0.5,
1.0,
2.0,
true,
);
assert!(s.is_finite(), "score must be finite, got {}", s);
}
}