3

I'm having a few issues with the fruchterman_reingold_force_directed_layout algorithm.

  • A lot of nodes always end up packing in the top-left corner.
  • If I use a non-rectangle topology, I get only NaNs for the positions.
  • The layout entirely changes depending on the initial positions ; other implementations I've seen seem to always converge to the same final positions.

Here is my layout code, what am I doing wrong ?:

using edge_property_t = boost::property<boost::edge_weight_t, float>;
using graph_t         = boost::adjacency_list<boost::vecS,
                                      boost::vecS,
                                      boost::directedS,
                                      boost::no_property,
                                      edge_property_t>;
using topology_type   = boost::rectangle_topology<std::mt19937>;
using point_type      = topology_type::point_type;

using pos_map = boost::iterator_property_map<
    std::vector<point_type>::iterator,
    boost::property_map<graph_t, boost::vertex_index_t>::type>;

graph_t graph_;
pos_map position_;
 
const auto N = 1234; // vertex count
std::vector<point_type> targetPositions_(N);

// ... vertices are added to the graph ... //
// ... positions are randomized in an initial step ... //

position_ = pos_map{targetPositions_.begin(), get(boost::vertex_index, graph_)};

topology_type topo{0,0,1000,1000};

const auto attraction =
    [this](auto e, double k, double d, const auto& g) -> double {
    return this->attraction_ * std::pow(d, this->attractionPower_) / k;
};
const auto repulsion =
    [this](auto v1, auto v2, double k, double d, const auto&) -> double {
    return this->repulsion_ * std::pow(k, this->repulsionPower_) / d;
};

const auto force_pairs = all_force_pairs();
const auto cooling = linear_cooling<double>(this->coolingIterations_, this->coolingTemp_);

auto displacements = std::vector<typename topology_type::point_difference_type>(N);
fruchterman_reingold_force_directed_layout(
    graph_, // graph_t
    position_, // pos_map
    topo,
    attraction,
    repulsion,
    force_pairs,
    cooling,
    make_iterator_property_map(displacements.begin(),
                               boost::identity_property_map{}));

I've tried all sort of values for my attraction and repulsion constants but am not able to get anything remotely satisfying.

Jean-Michaël Celerier
  • 7,412
  • 3
  • 54
  • 75

1 Answers1

0

I've taken your (original) code and made it self-contained. Then I proceeded to add frame-by-frame output. It does seem very hectic at high temperatures, but then when it cools off everything looks fine.

Here's a manageable size:

Live On Compiler Explorer

#include <boost/graph/adjacency_list.hpp>
#include <boost/graph/fruchterman_reingold.hpp>
#include <boost/graph/graphviz.hpp>
#include <boost/graph/random.hpp>
#include <boost/graph/random_layout.hpp>
#include <boost/graph/topology.hpp>
#include <boost/progress.hpp>
#include <random>

using edge_property_t = boost::property<boost::edge_weight_t, float>;
using graph_t         = boost::adjacency_list<boost::vecS,
                                    boost::vecS,
                                    boost::directedS,
                                    boost::no_property,
                                    edge_property_t>;
using topology_type   = boost::rectangle_topology<std::mt19937>;
using point_type      = topology_type::point_type;

using pos_map = boost::iterator_property_map<
    std::vector<point_type>::iterator,
    boost::property_map<graph_t, boost::vertex_index_t>::type>;

using Layout = std::vector<point_type>;

static void save_frame(std::string_view caption, int number, auto const& g,
                    auto const& posmap, topology_type const& topo) {
    std::stringstream graphprops;
    graphprops << std::setprecision(3) << std::fixed;

    {
        auto x1 = topo.origin()[0];
        auto y1 = topo.origin()[1];
        auto x2 = x1 + topo.extent()[0];
        auto y2 = y1 + topo.extent()[1];

        graphprops << "bb=\"" << x1 << "," << y1 << "," << x2 << "," << y2 << "\";\n";
    }
    graphprops << "label=" << std::quoted(caption) << ";\n";

    struct {
        decltype(g)                    g_;
        std::decay_t<decltype(posmap)> p_;
        std::string                    gp_;

        void operator()(std::ostream& os, graph_t::vertex_descriptor v) const {
            auto& pos = p_[v];

            os << "[label=vertex]";
            os << "[pos=\"" << pos[0] << "," << pos[1] << "!\"]";
        }
        void operator()(std::ostream& os, graph_t::edge_descriptor e) const {
            os << "[label=" << get(boost::edge_weight, g_, e) << "]";
        }
        void operator()(std::ostream& os) const { os << gp_; }
    } w{g, posmap, graphprops.str()};

    std::ofstream ofs("frame_" + std::to_string(number) + ".dot");
    ofs << std::setprecision(3) << std::fixed;
    write_graphviz(ofs, g, w, w, w);
}

struct QuestionCode {

    struct Impl {
        graph_t& graph_;
        Layout   position_storage_{boost::num_vertices(graph_)};

        pos_map position_ = boost::
            make_iterator_property_map( // prefer
                                        // make_safe_iterator_property_map!
                position_storage_.begin() /*, position_storage_.size()*/,
                get(boost::vertex_index, graph_));
    };

    float attraction_      = 0.5;
    int   attractionPower_ = 2;
    float repulsion_       = 0.6;
    int   repulsionPower_  = 3;

    size_t coolingIterations_ = 100;
    double coolingTemp_       = 5.0;

    Layout do_layout(graph_t& g) {
        // ... positions are randomized in an initial step ... //
        auto impl_ = std::make_unique<Impl>(Impl{g});

        //topology_type topo{0, 0, 10, 10};
        topology_type topo{-5, -5, 5, 5};
        random_graph_layout(impl_->graph_, impl_->position_, topo);

        const auto attraction =
            [this](auto /*e*/, double k, double d, const auto& /*g*/) -> double {
                return this->attraction_ * std::pow(d, this->attractionPower_) / k;
            };
        const auto repulsion =
            [this](auto /*v1*/, auto /*v2*/, double k, double d, const auto&) -> double {
                return this->repulsion_ * std::pow(k, this->repulsionPower_) / d;
            };

        boost::linear_cooling<double> linear_cooling(coolingIterations_, coolingTemp_);
        boost::progress_display progress(coolingIterations_ + 1, std::cerr);

        const auto cooling = [&, frame_number = 0]() mutable {
            ++progress;
            auto current_temp = linear_cooling();
            save_frame("temperature: " + std::to_string(current_temp),
                    frame_number++, impl_->graph_, impl_->position_, topo);
            return current_temp;
        };

        auto displacements =
            std::vector</*typename*/ topology_type::point_difference_type>(num_vertices(g));

        auto idmap = get(boost::vertex_index, g); // identity iff vecS

        fruchterman_reingold_force_directed_layout(
            impl_->graph_,            // graph_t
            impl_->position_,         // pos_map
            topo,                     //
            attraction,               //
            repulsion,                //
            boost::all_force_pairs{}, //
            cooling,                  //
            make_safe_iterator_property_map(displacements.begin(),
                                            displacements.size(), idmap));

        return impl_->position_storage_;
    }
};

int main() {
    graph_t g;

    std::mt19937 prng{std::random_device{}()};
    generate_random_graph(g, 10, 20, prng, false);
    std::uniform_real_distribution<float> weights(0, 5);
    for (auto e : boost::make_iterator_range(edges(g)))
        put(boost::edge_weight, g, e, weights(prng));

    Layout layout;
    {
        QuestionCode so;
        layout = so.do_layout(g);
    }
}

When testing with

rm frame_*; ./sotest; time (for a in {0..101}; do neato -Tpng -o frame_$a.png frame_$a.dot; done  && convert frame_{0..101}.png test.gif && gifsicle -O9 -k2 test.gif -o small.gif)

Shows

0%   10   20   30   40   50   60   70   80   90   100%
|----|----|----|----|----|----|----|----|----|----|
***************************************************

real    0m17,135s
user    0m20,336s
sys     0m1,655s

And a small.gif like: enter image description here

Perhaps you can use this testbed to tune your intuitions and perhaps see what you did differently. Always build with -fsanitize=undefined,address for sanity.

sehe
  • 374,641
  • 47
  • 450
  • 633
  • hmm, so for me this output is very much not good: things end up all stuck in the corners and the top line which is illegible. This is very much not supposed to look like this, more like https://www.researchgate.net/publication/301217160/figure/fig9/AS:359956277678080@1462831674860/Force-directed-layout-Fruchterman-Reingold-algorithm-of-an-example-ground-truth-network.png or https://www.researchgate.net/publication/327749850/figure/fig1/AS:672559553060867@1537362101485/Force-directed-plotting-with-Fruchterman-Reingold.png – Jean-Michaël Celerier Apr 22 '22 at 15:43
  • So i'll assume that there is an issue in the algorithm's implementation in boost somewhere.. will roll my own to see if I get things to look more correct (as what you have in your gif is similar to what I get on my graphs) – Jean-Michaël Celerier Apr 22 '22 at 15:44
  • (and, I've made myself an UI where I can fine-tune the parameters and consistently get this issue for every set of parameter I could try) – Jean-Michaël Celerier Apr 22 '22 at 15:48
  • Why do you assume that? From just looking at the first image, I **think** that the attraction/repulsion must have some kind of vertex-specific element? That's not what your code uses. – sehe Apr 22 '22 at 16:14
  • You should probably have made your question code self-contained (I could do it :)) and included examples of output and what you expected. I don't know what you expected people to do except what I did - reproduce what you already knew. (I don't resent it, it's just sad that it doesn't help you...) – sehe Apr 22 '22 at 16:16
  • Check out this video which explains the algorithm: there's no particular vertex-specific element yet things do not go packing into corners like they do with the boost implementation: https://www.youtube.com/watch?v=JAe7Oscsp98 – Jean-Michaël Celerier Apr 22 '22 at 20:30
  • It strikes me that the vertices getting sent to the extreme borders is an effect of high temperature. I don't know the "unit of temperature" (whether it relates to the topoloogy area e.g.), but If you start with "very high temperature" and "too few" steps to get a decent number of iterations below a useful temperature, the nodes might stay stuck on the extremes. See e.g. this simplified example https://compiler-explorer.com/z/Yn71zMohW results in this https://imgur.com/XRMKa3l. The only "usefully relaxed" iterations seem to have temp < 5 at which it quickly converges (for this simple graph). – sehe Apr 22 '22 at 22:24
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/244119/discussion-between-sehe-and-jean-michael-celerier). – sehe Apr 22 '22 at 22:24