Skip to content
358 changes: 358 additions & 0 deletions include/boost/graph/personalized_page_rank.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
// Copyright 2026 Emmanouil Krasanakis

// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE_1_0.txt or copy at
// http://www.boost.org/LICENSE_1_0.txt)

// Authors: Emmanouil Krasanakis

#ifndef BOOST_GRAPH_PERSONALIZED_PAGE_RANK_HPP
#define BOOST_GRAPH_PERSONALIZED_PAGE_RANK_HPP

#include <boost/property_map/property_map.hpp>
#include <boost/graph/graph_traits.hpp>
#include <boost/graph/properties.hpp>
#include <boost/graph/iteration_macros.hpp>
#include <boost/graph/overloading.hpp>
#include <boost/graph/detail/mpi_include.hpp>
#include <boost/property_map/function_property_map.hpp>
#include <vector>

namespace boost
{
namespace graph
{
namespace personalized_page_rank_detail
{
template < typename Graph>
std::size_t in_or_out_degree(
typename boost::graph_traits<Graph>::vertex_descriptor v,
const Graph& g,
bidirectional_graph_tag)
{
return in_degree(v, g);
}
template < typename Graph>
std::size_t in_or_out_degree(
typename boost::graph_traits<Graph>::vertex_descriptor v,
const Graph& g,
incidence_graph_tag)
{
return out_degree(v, g);
}
}

template <typename Graph>
boost::function_property_map<
std::function<double(typename boost::graph_traits<Graph>::edge_descriptor)>,
typename boost::graph_traits<Graph>::edge_descriptor>
markovian_weights(const Graph& g)
{
using Edge = typename boost::graph_traits<Graph>::edge_descriptor;
using Func = std::function<double(Edge)>;
Func f = [&g](Edge e)
{
auto u = source(e, g);
std::size_t denom;
denom = out_degree(u, g) ;
return 1.0 / denom;
};
return make_function_property_map<Edge, double>(f);
}

template <typename Graph>
boost::function_property_map<
std::function<double(typename boost::graph_traits<Graph>::edge_descriptor)>,
typename boost::graph_traits<Graph>::edge_descriptor>
spectral_weights(const Graph& g)
{
using Edge = typename boost::graph_traits<Graph>::edge_descriptor;
using Func = std::function<double(Edge)>;
Func f = [&g](Edge e)
{
typedef typename boost::graph_traits<Graph>::traversal_category category;
auto u = source(e, g);
auto v = target(e, g);
std::size_t denom;
denom = out_degree(u, g) * personalized_page_rank_detail::in_or_out_degree(v, g, category());
return 1.0 / std::sqrt(denom);
};
return make_function_property_map<Edge, double>(f);
}

template <typename Graph>
boost::function_property_map<
std::function<double(typename boost::graph_traits<Graph>::edge_descriptor)>,
typename boost::graph_traits<Graph>::edge_descriptor>
renormalized_weights(const Graph& g)
{
using Edge = typename boost::graph_traits<Graph>::edge_descriptor;
using Func = std::function<double(Edge)>;
Func f = [&g](Edge e)
{
typedef typename boost::graph_traits<Graph>::traversal_category category;
auto u = source(e, g);
auto v = target(e, g);
std::size_t denom;
denom = (1+out_degree(u, g)) * (1+personalized_page_rank_detail::in_or_out_degree(v, g, category()));
return 1.0 / std::sqrt(denom);
};
return make_function_property_map<Edge, double>(f);
}
Comment thread
Becheler marked this conversation as resolved.
Outdated

struct vertex_map_convergence
{
explicit vertex_map_convergence(std::size_t n, double tol=0) : n(n), tol(tol) {} // allowing tolerance for early stopping

template < typename RankMap, typename RankMap2, typename Graph >
bool operator()(
const RankMap& current,
const RankMap2& previous,
const Graph& g)
{
if ( n-- == 0 )
{
return true;
}
if (!tol)
{
return false;
}

typedef typename property_traits< RankMap >::value_type rank_type;
rank_type sum_abs(0);
for (auto v : boost::make_iterator_range(vertices(g)))
{
rank_type difference = get(current, v)-get(previous, v);
sum_abs += std::abs(difference);
}
return sum_abs*num_vertices(g)<tol;
}

std::size_t get_remaining_iters() const
{
return n;
}

bool has_converged() const
{
return n || !tol;
}

private:
std::size_t n;
double tol;
};
Comment thread
Becheler marked this conversation as resolved.
Outdated

namespace personalized_page_rank_detail
{
template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap,
typename RankMap2 >
void personalized_page_rank_step(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap from_rank,
RankMap2 to_rank,
typename property_traits< RankMap >::value_type damping,
incidence_graph_tag)
{
typedef typename property_traits< RankMap >::value_type rank_type;
rank_type l1_norm(0); // Computing the norm simultaneously avoids an extra summing iteration.

// Initialize the constant part of maps
for (auto v : boost::make_iterator_range(vertices(g)))
{
auto v_constant = rank_type(1 - damping) * get(personalization_map, v);
put(to_rank, v, v_constant);
l1_norm += v_constant;
}

for (auto u : boost::make_iterator_range(vertices(g)))
{
rank_type u_rank_factor = damping * get(from_rank, u);
rank_type l1_accumulated_norm(0); // TBD: Consider making l1_norm volatile to reduce accumulation errors.
for (auto e : boost::make_iterator_range(out_edges(u, g)))
{
auto v = target(e, g);
rank_type u_rank_out = get(weight_map, e)*u_rank_factor;
put(to_rank, v, get(to_rank, v) + u_rank_out);
l1_accumulated_norm += u_rank_out;
}
l1_norm += l1_accumulated_norm;
}
for (auto v : boost::make_iterator_range(vertices(g))) put(to_rank, v, get(to_rank, v)/l1_norm);
}

template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap,
typename RankMap2 >
void personalized_page_rank_step(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap from_rank,
RankMap2 to_rank,
typename property_traits< RankMap >::value_type damping,
bidirectional_graph_tag)
{
typedef typename property_traits< RankMap >::value_type damping_type;
damping_type l1_norm(0); // Computing the norm simultaneously avoids an extra summing iteration.
for (auto v : boost::make_iterator_range(vertices(g)))
{
typename property_traits< RankMap >::value_type rank(0);
for (auto e : boost::make_iterator_range(in_edges(v, g)))
rank += get(from_rank, source(e, g))*get(weight_map, e);
auto v_score = (damping_type(1) - damping) * get(personalization_map, v) + damping * rank;
put(to_rank, v, v_score);
l1_norm += v_score;
}
for (auto v : boost::make_iterator_range(vertices(g))) put(to_rank, v, get(to_rank, v)/l1_norm);
}
} // end namespace detail

template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap,
typename Done,
typename RankMap2 >
Done personalized_page_rank(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap rank_map,
Done done,
typename property_traits< RankMap >::value_type damping,
typename graph_traits< Graph >::vertices_size_type n,
RankMap2 rank_map2
BOOST_GRAPH_ENABLE_IF_MODELS_PARM(Graph, vertex_list_graph_tag))
{
typedef typename property_traits< PersonalizationMap >::value_type rank_type;

rank_type personalization_norm(0);
for (auto v : boost::make_iterator_range(vertices(g))) personalization_norm += get(personalization_map, v);

// TBD: This implementation couples iterators when possible under reduced L1 cache invalidation assumptions,
// but this is not necessarily the case because we may be grabbing 2x memory lanes each time to write there.
// Should investigate which pattern is faster.
Comment thread
Becheler marked this conversation as resolved.
Outdated
for (auto v : boost::make_iterator_range(vertices(g)))
{
rank_type value = get(personalization_map, v)/personalization_norm;
put(personalization_map, v, value);
put(rank_map, v, value);
}

bool to_map_2 = true;
do
{
typedef typename graph_traits< Graph >::traversal_category category;
if (to_map_2)
personalized_page_rank_detail::personalized_page_rank_step(g, weight_map, personalization_map, rank_map, rank_map2, damping, category());
else
personalized_page_rank_detail::personalized_page_rank_step(g, weight_map, personalization_map, rank_map2, rank_map, damping, category());
to_map_2 = !to_map_2;
} while ((to_map_2 && !done(rank_map, rank_map2, g)) || (!to_map_2 && !done(rank_map2, rank_map, g)));

// Now multiply the result with personalization_norm to restore the order of magnitude and store it in rank_map.
// Also restore the original personalization_map's magnitude for reuse (this is lossy up to numerical tolerance but leaner).'
if (!to_map_2)
{
for (auto v : boost::make_iterator_range(vertices(g)))
{
put(rank_map, v, get(rank_map2, v)*personalization_norm);
put(personalization_map, v, get(personalization_map, v)*personalization_norm);
}
}
else
{
for (auto v : boost::make_iterator_range(vertices(g)))
{
put(rank_map, v, get(rank_map, v)*personalization_norm);
put(personalization_map, v, get(personalization_map, v)*personalization_norm);
}
}
return done;
}

template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap,
typename Done >
Done personalized_page_rank(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap rank_map,
Done done,
typename property_traits< RankMap >::value_type damping,
typename graph_traits< Graph >::vertices_size_type n)
Comment thread
Becheler marked this conversation as resolved.
Outdated
{
typedef typename property_traits< RankMap >::value_type rank_type;
Comment thread
Becheler marked this conversation as resolved.
Outdated

std::vector< rank_type > ranks2(num_vertices(g));
return personalized_page_rank(g, weight_map, personalization_map, rank_map, done, damping, n,
make_iterator_property_map(ranks2.begin(), get(vertex_index, g)));
}

template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap,
typename Done >
inline Done personalized_page_rank(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap rank_map,
Done done,
typename property_traits< RankMap >::value_type damping = 0.85)
{
return personalized_page_rank(g, weight_map, personalization_map, rank_map, done, damping, num_vertices(g));
}

template <
typename Graph,
typename WeightMap,
typename PersonalizationMap,
typename RankMap >
inline vertex_map_convergence personalized_page_rank(
const Graph& g,
WeightMap weight_map,
PersonalizationMap personalization_map,
RankMap rank_map,
typename property_traits< RankMap >::value_type damping = 0.85)
{
std::size_t n_iters(1.0/(1-damping)+0.5); // accounts for "most" random walks
Comment thread
Becheler marked this conversation as resolved.
Outdated
auto convergence = vertex_map_convergence(n_iters);
return personalized_page_rank(g, weight_map, personalization_map, rank_map, convergence, damping, num_vertices(g));
}

template <
typename Graph,
typename PersonalizationMap,
typename RankMap >
inline vertex_map_convergence personalized_page_rank(
const Graph& g,
PersonalizationMap personalization_map,
RankMap rank_map,
typename property_traits< RankMap >::value_type damping = 0.85)
{
return personalized_page_rank(g, asymmetric_normalization(g), personalization_map, rank_map);
}

}
} // end namespace boost::graph

#endif // BOOST_GRAPH_PERSONALIZED_PAGE_RANK_HPP
1 change: 1 addition & 0 deletions test/Jamfile.v2
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ alias graph_test_regular :
[ run delete_edge.cpp ]
[ run johnson-test.cpp ]
[ run lvalue_pmap.cpp ]
[ run personalized_pagerank_test.cpp ]
;

alias graph_test_with_filesystem : :
Expand Down
Loading
Loading