VeloGraphX
High-performance dynamic graph analytics in C++20
Loading...
Searching...
No Matches
pagerank.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <algorithm>
4#include <cmath>
5#include <cstddef>
6#include <cstdint>
7#include <limits>
8#include <stdexcept>
9#include <vector>
10
13
14namespace velographx {
15
17 double l1_error{0.0};
18 double linf_error{0.0};
19 double local_residual_l1{0.0};
23 std::size_t reference_iterations{0};
25 bool within_tolerance{false};
26 bool fallback_applied{false};
27};
28
29template <class Graph>
31 public:
32 explicit BasicIncrementalPageRank(Graph& g, double damping = 0.85)
33 : g_(g), damping_(damping) {
34 if (!(damping_ >= 0.0 && damping_ <= 1.0)) {
35 throw std::invalid_argument("PageRank damping must be in [0, 1]");
36 }
37 recompute();
38 }
39
40 [[nodiscard]] const std::vector<double>& values() const noexcept { return rank_; }
41 [[nodiscard]] std::size_t last_repaired_vertices() const noexcept { return last_repaired_vertices_; }
42 [[nodiscard]] std::size_t last_repair_iterations() const noexcept { return last_repair_iterations_; }
43 [[nodiscard]] double last_residual_l1() const noexcept { return last_residual_l1_; }
44 [[nodiscard]] double last_residual_linf() const noexcept { return last_residual_linf_; }
45 [[nodiscard]] bool last_full_recompute_converged() const noexcept { return last_full_recompute_converged_; }
46
47 void apply(const UpdateBatch& batch, std::size_t local_iterations = 24,
48 double tol = 1e-9, double full_fallback_fraction = 0.60) {
49 if (batch.empty()) {
50 last_repaired_vertices_ = 0;
51 last_repair_iterations_ = 0;
52 last_residual_l1_ = 0.0;
53 last_residual_linf_ = 0.0;
54 return;
55 }
56
57 const auto previous_n = rank_.size();
58 const bool had_dangling_before = dangling_vertices_ != 0;
59
60 // Only these vertices can change outdegree in this batch. Tracking them
61 // keeps the ordinary local-repair path proportional to the update instead
62 // of introducing an O(V) dangling-status scan on every apply().
63 std::vector<VertexId> dangling_candidates;
64 dangling_candidates.reserve(batch.updates.size() * (is_directed(g_) ? 1 : 2));
65 for (const auto& e : batch.updates) {
66 dangling_candidates.push_back(e.src);
67 if (!is_directed(g_) && e.dst != e.src) dangling_candidates.push_back(e.dst);
68 }
69 std::sort(dangling_candidates.begin(), dangling_candidates.end());
70 dangling_candidates.erase(
71 std::unique(dangling_candidates.begin(), dangling_candidates.end()),
72 dangling_candidates.end());
73
74 apply_updates(g_, batch);
75 const auto n = vertex_count(g_);
76 if (n == 0) {
77 rank_.clear();
78 dangling_vertices_ = 0;
79 last_repaired_vertices_ = 0;
80 last_repair_iterations_ = 0;
81 last_residual_l1_ = 0.0;
82 last_residual_linf_ = 0.0;
83 last_full_recompute_converged_ = true;
84 return;
85 }
86
87 // A vertex-count change alters teleportation globally. Likewise, dangling
88 // mass is globally redistributed. If it existed before the update, or an
89 // outdegree-changing endpoint is dangling afterward, local repair is not
90 // an exact update of the PageRank fixed point.
91 if (n != previous_n || had_dangling_before) {
92 recompute();
93 return;
94 }
95 for (const auto v : dangling_candidates) {
96 if (v < n && neighbor_count(g_, v) == 0) {
97 recompute();
98 return;
99 }
100 }
101 dangling_vertices_ = 0;
102
103 std::vector<std::uint8_t> active(n, 0);
104 std::size_t active_count = 0;
105 auto activate = [&](VertexId v) {
106 if (v < n && !active[v]) {
107 active[v] = 1;
108 ++active_count;
109 }
110 };
111
112 for (const auto& e : batch.updates) {
113 activate(e.src);
114 activate(e.dst);
115 if (e.src < n) for_each_neighbor(g_, e.src, activate);
116 if (!is_directed(g_) && e.dst < n) for_each_neighbor(g_, e.dst, activate);
117 }
118
119 if (active_count == 0) {
120 last_repaired_vertices_ = 0;
121 last_repair_iterations_ = 0;
122 last_residual_l1_ = 0.0;
123 last_residual_linf_ = 0.0;
124 return;
125 }
126
127 const auto fallback_limit = static_cast<std::size_t>(
128 std::max(1.0, full_fallback_fraction * static_cast<double>(n)));
129 if (active_count >= fallback_limit) {
130 recompute();
131 return;
132 }
133
134 const double base = (1.0 - damping_) / static_cast<double>(n);
135 std::vector<std::uint8_t> ever_active = active;
136 last_repair_iterations_ = 0;
137 last_residual_l1_ = 0.0;
138 last_residual_linf_ = 0.0;
139 last_full_recompute_converged_ = false;
140
141 for (std::size_t it = 0; it < local_iterations && active_count != 0; ++it) {
142 ++last_repair_iterations_;
143 auto next_rank = rank_;
144 std::vector<std::uint8_t> next_active(n, 0);
145 std::size_t next_count = 0;
146 double iter_l1 = 0.0;
147 double iter_linf = 0.0;
148
149 auto activate_next = [&](VertexId v) {
150 if (v < n && !next_active[v]) {
151 next_active[v] = 1;
152 ++next_count;
153 ever_active[v] = 1;
154 }
155 };
156
157 for (VertexId v = 0; v < n; ++v) {
158 if (!active[v]) continue;
159
160 double incoming = 0.0;
161 for_each_in_neighbor(g_, v, [&](VertexId u) {
162 const auto out_degree = neighbor_count(g_, u);
163 if (out_degree != 0) incoming += rank_[u] / static_cast<double>(out_degree);
164 });
165
166 const double updated = base + damping_ * incoming;
167 const double delta = std::abs(updated - rank_[v]);
168 next_rank[v] = updated;
169 iter_l1 += delta;
170 iter_linf = std::max(iter_linf, delta);
171
172 if (delta > tol) for_each_neighbor(g_, v, activate_next);
173 }
174
175 rank_.swap(next_rank);
176 active.swap(next_active);
177 active_count = next_count;
178 last_residual_l1_ = iter_l1;
179 last_residual_linf_ = iter_linf;
180
181 std::size_t repaired = 0;
182 for (auto flag : ever_active) repaired += flag != 0;
183 if (repaired >= fallback_limit) {
184 recompute();
185 return;
186 }
187 }
188
189 last_repaired_vertices_ = 0;
190 for (auto flag : ever_active) last_repaired_vertices_ += flag != 0;
191 }
192
193 void recompute(std::size_t max_iterations = 200, double tol = 1e-12) {
194 const auto result = full_solve(max_iterations, tol);
195 rank_ = result.values;
196 dangling_vertices_ = count_dangling_vertices();
197 last_repaired_vertices_ = vertex_count(g_);
198 last_repair_iterations_ = result.iterations;
199 last_residual_l1_ = result.residual_l1;
200 last_residual_linf_ = result.residual_linf;
201 last_full_recompute_converged_ = result.converged;
202 }
203
205 std::size_t reference_max_iterations = 500,
206 double reference_tol = 1e-12,
207 double l1_tolerance = 1e-6,
208 double linf_tolerance = 1e-7) const {
209 const auto reference = full_solve(reference_max_iterations, reference_tol);
210 PageRankValidation validation;
211 validation.reference_iterations = reference.iterations;
212 validation.reference_residual_l1 = reference.residual_l1;
213 validation.reference_residual_linf = reference.residual_linf;
214 validation.reference_converged = reference.converged;
215 validation.local_residual_l1 = last_residual_l1_;
216 validation.local_residual_linf = last_residual_linf_;
217
218 const auto count = std::min(rank_.size(), reference.values.size());
219 for (std::size_t i = 0; i < count; ++i) {
220 const double error = std::abs(rank_[i] - reference.values[i]);
221 validation.l1_error += error;
222 validation.linf_error = std::max(validation.linf_error, error);
223 }
224 if (rank_.size() != reference.values.size()) {
225 validation.l1_error = std::numeric_limits<double>::infinity();
226 validation.linf_error = std::numeric_limits<double>::infinity();
227 }
228 validation.within_tolerance = reference.converged &&
229 validation.l1_error <= l1_tolerance &&
230 validation.linf_error <= linf_tolerance;
231 return validation;
232 }
233
235 const UpdateBatch& batch,
236 std::size_t local_iterations = 64,
237 double local_tol = 1e-10,
238 double full_fallback_fraction = 0.95,
239 std::size_t reference_max_iterations = 500,
240 double reference_tol = 1e-12,
241 double l1_tolerance = 1e-6,
242 double linf_tolerance = 1e-7) {
243 apply(batch, local_iterations, local_tol, full_fallback_fraction);
244 auto validation = validate_against_full(reference_max_iterations, reference_tol,
245 l1_tolerance, linf_tolerance);
246 if (!validation.within_tolerance) {
247 const auto reference = full_solve(reference_max_iterations, reference_tol);
248 rank_ = reference.values;
249 dangling_vertices_ = count_dangling_vertices();
250 last_repaired_vertices_ = vertex_count(g_);
251 last_repair_iterations_ = reference.iterations;
252 last_residual_l1_ = reference.residual_l1;
253 last_residual_linf_ = reference.residual_linf;
254 last_full_recompute_converged_ = reference.converged;
255 validation.fallback_applied = true;
256 }
257 return validation;
258 }
259
260 private:
261 struct FullSolveResult {
262 std::vector<double> values;
263 std::size_t iterations{0};
264 double residual_l1{0.0};
265 double residual_linf{0.0};
266 bool converged{false};
267 };
268
269 [[nodiscard]] std::size_t count_dangling_vertices() const {
270 std::size_t count = 0;
271 for (VertexId u = 0; u < vertex_count(g_); ++u) {
272 count += neighbor_count(g_, u) == 0;
273 }
274 return count;
275 }
276
277 [[nodiscard]] FullSolveResult full_solve(std::size_t max_iterations, double tol) const {
278 FullSolveResult result;
279 const auto n = vertex_count(g_);
280 if (n == 0) {
281 result.converged = true;
282 return result;
283 }
284
285 result.values.assign(n, 1.0 / static_cast<double>(n));
286 std::vector<std::size_t> out_degree(n, 0);
287 for (VertexId u = 0; u < n; ++u) out_degree[u] = neighbor_count(g_, u);
288
289 const double teleport = (1.0 - damping_) / static_cast<double>(n);
290 for (std::size_t it = 0; it < max_iterations; ++it) {
291 double dangling_mass = 0.0;
292 for (VertexId u = 0; u < n; ++u) {
293 if (out_degree[u] == 0) dangling_mass += result.values[u];
294 }
295 const double dangling_share = damping_ * dangling_mass / static_cast<double>(n);
296
297 std::vector<double> next(n, teleport + dangling_share);
298 for (VertexId v = 0; v < n; ++v) {
299 double incoming = 0.0;
300 for_each_in_neighbor(g_, v, [&](VertexId u) {
301 if (out_degree[u] != 0) incoming += result.values[u] / static_cast<double>(out_degree[u]);
302 });
303 next[v] += damping_ * incoming;
304 }
305
306 result.residual_l1 = 0.0;
307 result.residual_linf = 0.0;
308 for (std::size_t i = 0; i < n; ++i) {
309 const double delta = std::abs(next[i] - result.values[i]);
310 result.residual_l1 += delta;
311 result.residual_linf = std::max(result.residual_linf, delta);
312 }
313 result.values.swap(next);
314 result.iterations = it + 1;
315 if (result.residual_linf <= tol) {
316 result.converged = true;
317 break;
318 }
319 }
320 return result;
321 }
322
323 Graph& g_;
324 double damping_;
325 std::vector<double> rank_;
326 std::size_t dangling_vertices_{0};
327 std::size_t last_repaired_vertices_{0};
328 std::size_t last_repair_iterations_{0};
329 double last_residual_l1_{0.0};
330 double last_residual_linf_{0.0};
331 bool last_full_recompute_converged_{false};
332};
333
335
336} // namespace velographx
PageRankValidation apply_validated(const UpdateBatch &batch, std::size_t local_iterations=64, double local_tol=1e-10, double full_fallback_fraction=0.95, std::size_t reference_max_iterations=500, double reference_tol=1e-12, double l1_tolerance=1e-6, double linf_tolerance=1e-7)
Definition pagerank.hpp:234
void apply(const UpdateBatch &batch, std::size_t local_iterations=24, double tol=1e-9, double full_fallback_fraction=0.60)
Definition pagerank.hpp:47
std::size_t last_repaired_vertices() const noexcept
Definition pagerank.hpp:41
BasicIncrementalPageRank(Graph &g, double damping=0.85)
Definition pagerank.hpp:32
void recompute(std::size_t max_iterations=200, double tol=1e-12)
Definition pagerank.hpp:193
double last_residual_l1() const noexcept
Definition pagerank.hpp:43
PageRankValidation validate_against_full(std::size_t reference_max_iterations=500, double reference_tol=1e-12, double l1_tolerance=1e-6, double linf_tolerance=1e-7) const
Definition pagerank.hpp:204
const std::vector< double > & values() const noexcept
Definition pagerank.hpp:40
std::size_t last_repair_iterations() const noexcept
Definition pagerank.hpp:42
bool last_full_recompute_converged() const noexcept
Definition pagerank.hpp:45
double last_residual_linf() const noexcept
Definition pagerank.hpp:44
void apply_updates(Graph &graph, const Batch &batch)
void for_each_neighbor(const Graph &graph, VertexId u, Fn &&fn)
constexpr bool is_directed(const Graph &graph)
std::size_t neighbor_count(const Graph &graph, VertexId u)
std::uint32_t VertexId
Definition frontier.hpp:6
constexpr std::size_t vertex_count(const Graph &graph)
void for_each_in_neighbor(const Graph &graph, VertexId v, Fn &&fn)
bool empty() const noexcept
std::vector< EdgeUpdate > updates