|
11 | 11 | from typing import Any, Callable |
12 | 12 |
|
13 | 13 | import networkx as nx |
| 14 | +import torch |
14 | 15 |
|
15 | 16 | from network_diffusion.mln.actor import MLNetworkActor |
16 | 17 | from network_diffusion.mln.functions import all_neighbours |
@@ -310,3 +311,128 @@ def voterank_actorwise( |
310 | 311 | vote_rank[nbr][1] = max(vote_rank[nbr][1], 0) |
311 | 312 |
|
312 | 313 | return influential_actors |
| 314 | + |
| 315 | + |
| 316 | +def _build_edge_tensors( |
| 317 | + net: MultilayerNetwork, |
| 318 | + actor_to_idx: dict[MLNetworkActor, int], |
| 319 | + device: torch.device, |
| 320 | +) -> tuple[torch.Tensor, torch.Tensor]: |
| 321 | + """ |
| 322 | + Build undirected edge index tensors from a multilayer network. |
| 323 | +
|
| 324 | + Each edge (a, b) is represented twice — as (a→b) and (b→a) — so that |
| 325 | + scatter-based aggregation covers both directions without special-casing. |
| 326 | +
|
| 327 | + :param net: multilayer network to extract edges from. |
| 328 | + :param actor_to_idx: mapping from actor to its integer index. |
| 329 | + :param device: torch device to place the resulting tensors on. |
| 330 | + :return: tuple (edge_src, edge_dst), each a 1-D long tensor of length 2E, |
| 331 | + where E is the number of undirected edges in the network. |
| 332 | + """ |
| 333 | + links = list(net.get_links()) |
| 334 | + |
| 335 | + if not links: |
| 336 | + empty = torch.empty(0, dtype=torch.long, device=device) |
| 337 | + return empty, empty |
| 338 | + |
| 339 | + src = torch.tensor([actor_to_idx[a] for a, _ in links], dtype=torch.long) |
| 340 | + dst = torch.tensor([actor_to_idx[b] for _, b in links], dtype=torch.long) |
| 341 | + |
| 342 | + edge_src = torch.cat([src, dst]).to(device) |
| 343 | + edge_dst = torch.cat([dst, src]).to(device) |
| 344 | + |
| 345 | + return edge_src, edge_dst |
| 346 | + |
| 347 | + |
| 348 | +def _get_device(device: str | None) -> torch.device: |
| 349 | + """ |
| 350 | + Resolve device string to torch.device, auto-detecting if not provided. |
| 351 | +
|
| 352 | + :param device: device string (e.g. ``"cuda"``, ``"mps"``, ``"cpu"``, |
| 353 | + ``"cuda:1"``), or ``None`` for auto-detection. |
| 354 | + :return: resolved :class:`torch.device` instance. |
| 355 | + """ |
| 356 | + if device: |
| 357 | + return torch.device(device) |
| 358 | + if torch.cuda.is_available(): |
| 359 | + return torch.device("cuda") |
| 360 | + if torch.backends.mps.is_available(): |
| 361 | + return torch.device("mps") |
| 362 | + return torch.device("cpu") |
| 363 | + |
| 364 | + |
| 365 | +def torch_voterank_actorwise( |
| 366 | + net: MultilayerNetwork, |
| 367 | + number_of_actors: int | None = None, |
| 368 | + device: str | None = None, |
| 369 | +) -> list[MLNetworkActor]: |
| 370 | + """ |
| 371 | + Select a list of influential ACTORS in a graph using VoteRank algorithm. |
| 372 | +
|
| 373 | + VoteRank computes a ranking of the actors in a graph based on a voting |
| 374 | + scheme. With VoteRank, all actors vote for each of its neighbours and the |
| 375 | + actor with the highest votes is elected iteratively. The voting ability of |
| 376 | + neighbors of elected actors is decreased in subsequent turns. |
| 377 | +
|
| 378 | + Optimized with PyTorch tensors — supports CUDA, MPS (float32), and CPU. |
| 379 | +
|
| 380 | + :param net: multilayer network. |
| 381 | + :param number_of_actors: number of ranked actors to extract (default all). |
| 382 | + :param device: torch device string e.g. "cuda", "mps", "cpu". |
| 383 | + Auto-detected if not provided (CUDA > MPS > CPU). |
| 384 | + :return: ordered list of computed seeds, only actors with positive number |
| 385 | + of votes are returned. |
| 386 | + """ |
| 387 | + if net.is_directed(): |
| 388 | + raise NotImplementedError( |
| 389 | + "Voterank for directed networks is not implemented!" |
| 390 | + ) |
| 391 | + if len(net) == 0: |
| 392 | + return [] |
| 393 | + |
| 394 | + # Step 1 actors — snapshot once, single source of truth for ordering |
| 395 | + actors: list[MLNetworkActor] = list(net.get_actors()) |
| 396 | + n = len(actors) |
| 397 | + actor_to_idx = {a: i for i, a in enumerate(actors)} |
| 398 | + number_of_actors = min(number_of_actors or n, n) |
| 399 | + # Step 2 Build edge tensors |
| 400 | + edge_src, edge_dst = _build_edge_tensors( |
| 401 | + net=net, |
| 402 | + actor_to_idx=actor_to_idx, |
| 403 | + device=_get_device(device), |
| 404 | + ) |
| 405 | + |
| 406 | + inv_avg_nbs = torch.tensor( |
| 407 | + n / sum(neighbourhood_size(net=net).values()), |
| 408 | + dtype=torch.float32, |
| 409 | + device=edge_src.device, |
| 410 | + ) |
| 411 | + # Step 3 VoteRank state |
| 412 | + scores = torch.zeros(n, dtype=torch.float32, device=edge_src.device) |
| 413 | + ability = torch.ones(n, dtype=torch.float32, device=edge_src.device) |
| 414 | + elected = torch.zeros(n, dtype=torch.bool, device=edge_src.device) |
| 415 | + |
| 416 | + influential_actors: list[MLNetworkActor] = [] |
| 417 | + |
| 418 | + for _ in range(number_of_actors): |
| 419 | + scores.zero_() |
| 420 | + scores.scatter_add_(0, edge_dst, ability[edge_src]) |
| 421 | + scores[elected] = 0.0 |
| 422 | + |
| 423 | + # Step 4 select top actor |
| 424 | + top_idx = int(scores.argmax().item()) |
| 425 | + if scores[top_idx].item() == 0.0: |
| 426 | + break |
| 427 | + |
| 428 | + influential_actors.append(actors[top_idx]) |
| 429 | + elected[top_idx] = True |
| 430 | + ability[top_idx] = 0.0 |
| 431 | + |
| 432 | + # Step 5 weaken neighbours of elected actor |
| 433 | + neighbour_mask = edge_dst[edge_src == top_idx] |
| 434 | + if neighbour_mask.numel() > 0: |
| 435 | + ability[neighbour_mask] -= inv_avg_nbs |
| 436 | + ability.clamp_(min=0.0) |
| 437 | + |
| 438 | + return influential_actors |
0 commit comments