Files
SAPFOR/Sapfor/_src/VisualizerCalls/graphLayout/kamada_kawai.cpp
2025-03-12 12:37:19 +03:00

189 lines
6.8 KiB
C++

#include <algorithm>
#include <cassert>
#include <cmath>
#include <limits>
#include "algebra.hpp"
#include "kamada_kawai.hpp"
namespace nodesoup {
using std::vector;
KamadaKawai::KamadaKawai(const adj_list_t& g, double k, double energy_threshold)
: g_(g)
, energy_threshold_(energy_threshold) {
vector<vector<vertex_id_t>> distances = floyd_warshall_(g_);
// find biggest distance
size_t biggest_distance = 0;
for (vertex_id_t v_id = 0; v_id < g_.size(); v_id++) {
for (vertex_id_t other_id = 0; other_id < g_.size(); other_id++) {
if (distances[v_id][other_id] > biggest_distance) {
biggest_distance = distances[v_id][other_id];
}
}
}
// Ideal length for all edges. we don't really care, the layout is going to be scaled.
// Let's chose 1.0 as the initial positions will be on a 1.0 radius circle, so we're
// on the same order of magnitude
double length = 1.0 / biggest_distance;
// init springs lengths and strengths matrices
for (vertex_id_t v_id = 0; v_id < g_.size(); v_id++) {
vector<Spring> v_springs;
for (vertex_id_t other_id = 0; other_id < g_.size(); other_id++) {
Spring spring;
if (v_id == other_id) {
spring.length = 0.0;
spring.strength = 0.0;
} else {
size_t distance = distances[v_id][other_id];
spring.length = distance * length;
spring.strength = k / (distance * distance);
}
v_springs.push_back(spring);
}
springs_.push_back(v_springs);
}
}
vector<vector<vertex_id_t>> KamadaKawai::floyd_warshall_(const adj_list_t& g) {
// build adjacency matrix (infinity = no edge, 1 = edge)
unsigned int infinity = std::numeric_limits<unsigned int>::max() / 2;
vector<vector<vertex_id_t>> distances(g.size(), vector<vertex_id_t>(g.size(), infinity));
for (vertex_id_t v_id = 0; v_id < g.size(); v_id++) {
distances[v_id][v_id] = 0;
for (vertex_id_t adj_id : g[v_id]) {
if (adj_id > v_id) {
distances[v_id][adj_id] = 1;
distances[adj_id][v_id] = 1;
}
}
}
// floyd warshall itself, find length of shortest path for each pair of vertices
for (vertex_id_t k = 0; k < g.size(); k++) {
for (vertex_id_t i = 0; i < g.size(); i++) {
for (vertex_id_t j = 0; j < g.size(); j++) {
distances[i][j] = std::min(distances[i][j], distances[i][k] + distances[k][j]);
}
}
}
return distances;
}
#define MAX_VERTEX_ITERS_COUNT 50
#define MAX_STEADY_ENERGY_ITERS_COUNT 50
/**
Reduce the energy of the next vertex with most energy until all the vertices have
a energy below energy_threshold
*/
void KamadaKawai::operator()(vector<Point2D>& positions) const {
vertex_id_t v_id;
unsigned int steady_energy_count = 0;
double max_vertex_energy = find_max_vertex_energy_(positions, v_id);
while (max_vertex_energy > energy_threshold_ && steady_energy_count < MAX_STEADY_ENERGY_ITERS_COUNT) {
// move vertex step by step until its energy goes below threshold
// (apparently this is equivalent to the newton raphson method)
unsigned int vertex_count = 0;
do {
positions[v_id] = compute_next_vertex_position_(v_id, positions);
vertex_count++;
} while (compute_vertex_energy_(v_id, positions) > energy_threshold_ && vertex_count < MAX_VERTEX_ITERS_COUNT);
double max_vertex_energy_prev = max_vertex_energy;
max_vertex_energy = find_max_vertex_energy_(positions, v_id);
if (std::abs(max_vertex_energy - max_vertex_energy_prev) < 1e-20) {
steady_energy_count++;
} else {
steady_energy_count = 0;
}
}
}
/**
Find @p max_energy_v_id with the most potential energy and @return its energy
// https://gist.github.com/terakun/b7eff90c889c1485898ec9256ca9f91d
*/
double KamadaKawai::find_max_vertex_energy_(const vector<Point2D>& positions, vertex_id_t& max_energy_v_id) const {
double max_energy = -1.0;
for (vertex_id_t v_id = 0; v_id < g_.size(); v_id++) {
double energy = compute_vertex_energy_(v_id, positions);
if (energy > max_energy) {
max_energy_v_id = v_id;
max_energy = energy;
}
}
assert(max_energy != -1.0);
return max_energy;
}
/** @return the potential energies of springs between @p v_id and all other vertices */
double KamadaKawai::compute_vertex_energy_(vertex_id_t v_id, const vector<Point2D>& positions) const {
double x_energy = 0.0;
double y_energy = 0.0;
for (vertex_id_t other_id = 0; other_id < g_.size(); other_id++) {
if (v_id == other_id) {
continue;
}
Vector2D delta = positions[v_id] - positions[other_id];
double distance = delta.norm();
// delta * k * (1 - l / distance)
Spring spring = springs_[v_id][other_id];
x_energy += delta.dx * spring.strength * (1.0 - spring.length / distance);
y_energy += delta.dy * spring.strength * (1.0 - spring.length / distance);
}
return sqrt(x_energy * x_energy + y_energy * y_energy);
}
/**
@returns next position for @param v_id reducing its potential energy, ie the energy in the whole graph
caused by its position.
The position's delta depends on K (TODO bigger K = faster?).
This is the complicated part of the algorithm.
*/
Point2D KamadaKawai::compute_next_vertex_position_(vertex_id_t v_id, const vector<Point2D>& positions) const {
double xx_energy = 0.0, xy_energy = 0.0, yx_energy = 0.0, yy_energy = 0.0;
double x_energy = 0.0, y_energy = 0.0;
for (vertex_id_t other_id = 0; other_id < g_.size(); other_id++) {
if (v_id == other_id) {
continue;
}
Vector2D delta = positions[v_id] - positions[other_id];
double distance = delta.norm();
double cubed_distance = distance * distance * distance;
Spring spring = springs_[v_id][other_id];
x_energy += delta.dx * spring.strength * (1.0 - spring.length / distance);
y_energy += delta.dy * spring.strength * (1.0 - spring.length / distance);
xy_energy += spring.strength * spring.length * delta.dx * delta.dy / cubed_distance;
xx_energy += spring.strength * (1.0 - spring.length * delta.dy * delta.dy / cubed_distance);
yy_energy += spring.strength * (1.0 - spring.length * delta.dx * delta.dx / cubed_distance);
}
yx_energy = xy_energy;
Point2D position = positions[v_id];
double denom = xx_energy * yy_energy - xy_energy * yx_energy;
position.x += (xy_energy * y_energy - yy_energy * x_energy) / denom;
position.y += (xy_energy * x_energy - xx_energy * y_energy) / denom;
return position;
}
}