OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / WpfDisplay / Helper / ForceDirectedGraphLayout.cs

74 lines · csharp ref:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

namespace WpfDisplay.Helper;

/// <summary>
/// Naive implementation of Force-Directed Graph Layout.
/// <a href="https://www2.eecs.berkeley.edu/Pubs/TechRpts/2013/EECS-2013-176.pdf">Algorithm description</a>.
/// <a href="http://yifanhu.net/PUB/graph_draw.pdf">Original paper</a>.
/// "Intuitively, every vertex wants to keep its neighbors close while pushing all other vertices away."
/// O(V^2), but O(V) in practice
/// </summary>
public static class ForceDirectedGraphLayout
{
    public record Graph(List<Vector2> Vertices, List<(int, int)> Edges);

    /// <summary>
    /// 
    /// </summary>
    /// <param name="g"></param>
    /// <param name="K">Optimal distance, aka. natural spring length.</param>
    /// <param name="C">Relative strength of the attractive and repulsive forces.</param>
    /// <param name="tol">Termination tolerance. > 0</param>
    /// <returns></returns>
    public static List<Vector2> GenerateLayout(Graph g, double K, double C, double tol)
    {
        //TODO: Kmult
        K *= C * Math.Sqrt(500 * 500 / g.Vertices.Count);

        List<Vector2> x = g.Vertices.ToList();
        double step = 50;//initial step length. say 1/10 of canvas width
        bool converged = false;
        while (!converged)//TODO: max 600
        {//iteratively minimize the system energy
            var x0 = x.ToList();
            for (int i = 0; i < g.Vertices.Count; i++)
            {
                Vector2 f = new Vector2(0, 0);
                foreach (var e in g.Edges.Where(e => e.Item1 == i))
                {
                    var v1 = g.Vertices[e.Item1];
                    var v2 = g.Vertices[e.Item2];
                    float af = (float)(CalculateAttractiveForce((v1, v2), K) / Vector2.Distance(v1, v2));
                    f += Vector2.Multiply(af, v2 - v1);
                }
                for (int j = 0; j < g.Vertices.Count; j++)
                {
                    if (i == j)
                        continue;
                    var v1 = g.Vertices[i];
                    var v2 = g.Vertices[j];
                    float rf = (float)(CalculateRepulsiveForce(v1, v2, C, K) / Vector2.Distance(v1, v2));
                    f += Vector2.Multiply(rf, v2 - v1);
                }
                x[i] += Vector2.Multiply(f, (float)step / f.Length());
            }
            step *= 0.9;
            if (x.Zip(x0).Select(p => Vector2.Distance(p.First, p.Second)).Sum() < K * tol)
                converged = true;
        }
        return x;
    }

    // i =/= j
    private static double CalculateRepulsiveForce(Vector2 i, Vector2 j, double C, double K)
        => -C * K * K / Vector2.Distance(i, j);

    private static double CalculateAttractiveForce((Vector2 i, Vector2 j) edge, double K)
        => Math.Pow(Vector2.Distance(edge.i, edge.j), 2) / K;

}