#![allow(clippy::needless_range_loop)]
use alloc::vec::Vec;
use barnes_hut_tree::BarnesHutTree;
use num_traits::Float;
mod constants {
pub const TIME_STEP: f64 = 0.25;
pub const PREPROC_TIME_STEP: f64 = 0.5;
pub const PREPROC_ITERATIONS: usize = 20;
pub const PROTECTION: f64 = 0.25;
pub const COOL_DOWN: f64 = 0.999;
pub const MIN_ITERATIONS: usize = 4;
pub const STOP_CRIT_CONST_SQ: f64 = 2_000_400.0;
pub const MULTIPOLE_THRESHOLD: usize = 100;
pub const HIGH_DEGREE_GUARD: usize = 100;
}
#[derive(Debug, Clone, Copy)]
struct EdgeInfo<F> {
a: usize,
b: usize,
e: F,
}
#[derive(Debug)]
pub struct FastMultipoleEmbedder<F> {
tree: BarnesHutTree<F, u64, 2>,
forces: Vec<F>,
rep: Vec<F>,
edges: Vec<EdgeInfo<F>>,
degree: Vec<F>,
}
impl<F> Default for FastMultipoleEmbedder<F>
where
F: Float
+ num_traits::float::FloatCore
+ Send
+ Sync
+ core::ops::AddAssign
+ core::ops::SubAssign,
{
fn default() -> Self {
Self::new()
}
}
impl<F> FastMultipoleEmbedder<F>
where
F: Float
+ num_traits::float::FloatCore
+ Send
+ Sync
+ core::ops::AddAssign
+ core::ops::SubAssign,
{
#[must_use]
pub fn new() -> Self {
Self {
tree: BarnesHutTree::empty(),
forces: Vec::new(),
rep: Vec::new(),
edges: Vec::new(),
degree: Vec::new(),
}
}
#[allow(clippy::too_many_arguments)]
pub fn run_with_factors(
&mut self,
positions: &mut [F],
edges: &[(usize, usize)],
node_sizes: &[F],
iterations: usize,
theta: F,
rep_factor: F,
edge_factor: F,
) {
let n = node_sizes.len();
debug_assert_eq!(positions.len(), n * 2, "positions must be n * 2 long");
if n == 0 {
return;
}
if n == 1 {
for value in positions.iter_mut().take(2) {
*value = F::zero();
}
return;
}
self.edges.clear();
self.edges.reserve(edges.len());
self.degree.clear();
self.degree.resize(n, F::zero());
for &(a, b) in edges {
let e = node_sizes[a] + node_sizes[b];
self.edges.push(EdgeInfo { a, b, e });
self.degree[a] += F::one();
self.degree[b] += F::one();
}
self.forces.clear();
self.forces.resize(n * 2, F::zero());
let avg_node_size =
node_sizes.iter().copied().fold(F::zero(), |acc, s| acc + s) / F::from(n).unwrap();
let stop_force = F::from(n).unwrap() * F::from(n).unwrap() * avg_node_size
/ F::from(constants::STOP_CRIT_CONST_SQ).unwrap();
if n < constants::MULTIPOLE_THRESHOLD {
self.run_exact(positions, node_sizes, n, iterations, stop_force);
} else {
self.run_multipole(
positions,
node_sizes,
n,
iterations,
theta,
stop_force,
rep_factor,
edge_factor,
);
}
}
fn run_exact(
&mut self,
positions: &mut [F],
node_sizes: &[F],
n: usize,
max_iterations: usize,
stop_force: F,
) {
let t = F::from(constants::TIME_STEP).unwrap();
for _ in 0..constants::PREPROC_ITERATIONS {
self.zero_forces(n);
self.edge_forces(positions, F::one());
move_nodes::<F>(positions, &self.forces, t, F::one(), n);
}
for i in 0..max_iterations {
self.zero_forces(n);
self.repulsion_exact(positions, node_sizes, n, F::one());
self.edge_forces(positions, F::one());
let max_force_sq = move_nodes::<F>(positions, &self.forces, t, F::one(), n);
if max_force_sq < stop_force && i > constants::MIN_ITERATIONS {
break;
}
}
}
#[allow(clippy::too_many_arguments)]
fn run_multipole(
&mut self,
positions: &mut [F],
node_sizes: &[F],
n: usize,
max_iterations: usize,
theta: F,
stop_force: F,
rep_factor: F,
edge_factor: F,
) {
let preproc_t = F::from(constants::PREPROC_TIME_STEP).unwrap();
let base_t = F::from(constants::TIME_STEP).unwrap();
let cool_rate = F::from(constants::COOL_DOWN).unwrap();
for _ in 0..constants::PREPROC_ITERATIONS {
self.zero_forces(n);
self.edge_forces(positions, edge_factor);
move_nodes::<F>(positions, &self.forces, preproc_t, preproc_t, n);
}
let mut cool_down = F::one();
for i in 0..max_iterations {
cool_down = cool_down * cool_rate;
self.zero_forces(n);
self.repulsion_multipole(positions, node_sizes, n, theta, rep_factor);
self.edge_forces(positions, edge_factor);
let step = base_t * cool_down;
let max_disp_sq = move_nodes::<F>(positions, &self.forces, step, step, n);
if max_disp_sq < stop_force && i >= constants::MIN_ITERATIONS {
break;
}
}
}
fn zero_forces(&mut self, n: usize) {
for value in self.forces.iter_mut().take(n * 2) {
*value = F::zero();
}
}
fn repulsion_exact(&mut self, positions: &[F], node_sizes: &[F], n: usize, factor: F) {
let protection = F::from(constants::PROTECTION).unwrap();
for i in 0..n {
for j in (i + 1)..n {
let mut disp = [F::zero(); 2];
let mut dsq = F::zero();
for d in 0..2 {
let value = positions[i * 2 + d] - positions[j * 2 + d];
disp[d] = value;
dsq += value * value;
}
let s_sum = node_sizes[i] + node_sizes[j];
let denom = Float::max(s_sum * protection, dsq);
let f = (s_sum / denom) * factor;
for d in 0..2 {
let push = disp[d] * f;
self.forces[i * 2 + d] += push;
self.forces[j * 2 + d] -= push;
}
}
}
}
fn repulsion_multipole(
&mut self,
positions: &[F],
node_sizes: &[F],
n: usize,
theta: F,
factor: F,
) {
self.tree.rebuild(positions, node_sizes);
let mean_size =
node_sizes.iter().copied().fold(F::zero(), |acc, s| acc + s) / F::from(n).unwrap();
let floor = F::from(constants::PROTECTION).unwrap() * (mean_size + mean_size);
let two = F::from(2.0).unwrap();
let kernel = |displacement: &[F; 2],
distance_squared: F,
mass: F,
is_leaf: bool,
force: &mut [F; 2]| {
let charge = if is_leaf { mass * two } else { mass };
let f = charge / Float::max(distance_squared, floor);
for d in 0..2 {
force[d] += displacement[d] * f;
}
};
if self.rep.len() != n * 2 {
self.rep.resize(n * 2, F::zero());
}
self.tree.accumulate_all(positions, theta, &kernel, &mut self.rep);
let guard = F::from(constants::HIGH_DEGREE_GUARD).unwrap();
for i in 0..n {
let scale = if self.degree[i] > guard { factor / self.degree[i] } else { factor };
for d in 0..2 {
self.forces[i * 2 + d] += self.rep[i * 2 + d] * scale;
}
}
}
fn edge_forces(&mut self, positions: &[F], factor: F) {
let quarter = F::from(0.25).unwrap();
let half = F::from(0.5).unwrap();
for edge in &self.edges {
let a = edge.a;
let b = edge.b;
let mut disp = [F::zero(); 2];
let mut dsq = F::zero();
for d in 0..2 {
let value = positions[a * 2 + d] - positions[b * 2 + d];
disp[d] = value;
dsq += value * value;
}
let f = if dsq == F::zero() {
F::zero()
} else {
(dsq.ln() * half - edge.e.ln()) * quarter * factor
};
let fa = f / self.degree[a];
let fb = f / self.degree[b];
for d in 0..2 {
self.forces[a * 2 + d] -= disp[d] * fa;
self.forces[b * 2 + d] += disp[d] * fb;
}
}
}
}
fn move_nodes<F: Float>(positions: &mut [F], forces: &[F], step: F, metric: F, n: usize) -> F {
let mut max_sq = F::zero();
for i in 0..n {
let mut measured_sq = F::zero();
for d in 0..2 {
let f = forces[i * 2 + d];
let measured = f * metric;
measured_sq = measured_sq + measured * measured;
positions[i * 2 + d] = positions[i * 2 + d] + f * step;
}
if measured_sq > max_sq {
max_sq = measured_sq;
}
}
max_sq
}
#[cfg(test)]
mod tests {
#![allow(clippy::cast_precision_loss)]
use alloc::vec;
use super::*;
fn node_size() -> f64 {
(1.0 / 65.0) * (2.0f64).sqrt() / 2.0
}
#[test]
fn matches_ogdf_star_oracle() {
let s = node_size();
let mut positions = vec![0.05, 0.05, 1.0, 0.0, -1.0, 0.1, 0.1, 1.0, 0.0, -1.0];
let edges = [(0, 1), (0, 2), (0, 3), (0, 4)];
let sizes = vec![s; 5];
let mut fme = FastMultipoleEmbedder::<f64>::new();
fme.run_with_factors(&mut positions, &edges, &sizes, 5, 0.5, 2.0, 0.5);
let expected = [
0.035439838,
0.035867669,
0.320673585,
0.024086071,
-0.248787373,
0.052644137,
0.052533429,
0.320791483,
0.024114916,
-0.248680934,
];
for (got, want) in positions.iter().zip(expected.iter()) {
assert!(
(got - want).abs() < 1e-5,
"FME diverged from OGDF oracle: got {got}, want {want}"
);
}
}
#[test]
fn path_unfolds_to_a_line() {
let s = node_size();
let n = 8;
let mut positions = Vec::new();
for i in 0..n {
positions.push(i as f64 * 0.4);
positions.push(if i % 2 == 0 { -0.2 } else { 0.2 });
}
let edges: Vec<(usize, usize)> = (0..n - 1).map(|i| (i, i + 1)).collect();
let sizes = vec![s; n];
let mut fme = FastMultipoleEmbedder::<f64>::new();
fme.run_with_factors(&mut positions, &edges, &sizes, 60, 0.5, 2.0, 0.5);
let xs: Vec<f64> = (0..n).map(|i| positions[i * 2]).collect();
let ys: Vec<f64> = (0..n).map(|i| positions[i * 2 + 1]).collect();
let x_range = xs.iter().copied().fold(f64::MIN, f64::max)
- xs.iter().copied().fold(f64::MAX, f64::min);
let y_range = ys.iter().copied().fold(f64::MIN, f64::max)
- ys.iter().copied().fold(f64::MAX, f64::min);
assert!(x_range > 10.0 * y_range, "path did not flatten: x={x_range}, y={y_range}");
}
#[test]
fn multipole_path_stays_finite() {
let s = node_size();
let n = 200;
let spine = n / 2;
let mut positions = Vec::new();
let mut seed = 0x1234_5678u64;
let mut rng = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed >> 11) as f64 / (1u64 << 53) as f64
};
for _ in 0..n {
positions.push(rng() * 4.0 - 2.0);
positions.push(rng() * 4.0 - 2.0);
}
let mut edges = Vec::new();
for i in 0..spine - 1 {
edges.push((i, i + 1));
}
for i in 0..spine {
edges.push((i, spine + i));
}
let sizes = vec![s; n];
let mut fme = FastMultipoleEmbedder::<f64>::new();
fme.run_with_factors(&mut positions, &edges, &sizes, 100, 0.3, 2.0, 0.5);
assert!(positions.iter().all(|value| value.is_finite()));
}
}