2
2
3
3
import torch
4
4
5
+ from torch_geometric .utils import coalesce
6
+ from torch_geometric .data import Data
7
+
8
+ from pathpyG .algorithms .lift_order import aggregate_edge_index
5
9
from pathpyG .core .graph import Graph
10
+ from pathpyG .core .index_map import IndexMap
11
+ from pathpyG .core .multi_order_model import MultiOrderModel
12
+ from pathpyG .core .temporal_graph import TemporalGraph
6
13
7
14
8
15
def generate_bipartite_edge_index (g : Graph , g2 : Graph , mapping : str = "last" ) -> torch .Tensor :
@@ -22,3 +29,61 @@ def generate_bipartite_edge_index(g: Graph, g2: Graph, mapping: str = "last") ->
22
29
)
23
30
24
31
return bipartide_edge_index
32
+
33
+
34
+ def generate_second_order_model (g : TemporalGraph , delta : float | int = 1 , weight : str = "edge_weight" ) -> MultiOrderModel :
35
+ """
36
+ Generate a multi-order model with first- and second-order layer from a temporal graph.
37
+ This method is optimized for the memory footprint of large graphs and may be slower than creating small models with the default apporach.
38
+ """
39
+ data = g .data .sort_by_time ()
40
+ edge_index1 , timestamps1 = data .edge_index , data .time
41
+
42
+ node_sequence1 = torch .arange (data .num_nodes , device = edge_index1 .device ).unsqueeze (1 )
43
+ if weight in data :
44
+ edge_weight = data [weight ]
45
+ else :
46
+ edge_weight = torch .ones (edge_index1 .size (1 ), device = edge_index1 .device )
47
+
48
+ layer1 = aggregate_edge_index (
49
+ edge_index = edge_index1 , node_sequence = node_sequence1 , edge_weight = edge_weight
50
+ )
51
+ layer1 .mapping = g .mapping
52
+
53
+ node_sequence2 = torch .cat ([node_sequence1 [edge_index1 [0 ]], node_sequence1 [edge_index1 [1 ]][:, - 1 :]], dim = 1 )
54
+ node_sequence2 , edge1_to_node2 = torch .unique (node_sequence2 , dim = 0 , return_inverse = True )
55
+
56
+ edge_index2 = []
57
+ edge_weight2 = []
58
+ for timestamp in timestamps1 .unique ():
59
+ src_nodes2 , src_nodes2_counts = edge1_to_node2 [timestamps1 == timestamp ].unique (return_counts = True )
60
+ dst_nodes2 , dst_nodes2_counts = edge1_to_node2 [(timestamps1 > timestamp ) & (timestamps1 <= timestamp + delta )].unique (return_counts = True )
61
+ for src_node2 , src_node2_count in zip (src_nodes2 , src_nodes2_counts ):
62
+ dst_node2 = dst_nodes2 [node_sequence2 [dst_nodes2 , 0 ] == node_sequence2 [src_node2 , - 1 ]]
63
+ dst_node2_count = dst_nodes2_counts [node_sequence2 [dst_nodes2 , 0 ] == node_sequence2 [src_node2 , - 1 ]]
64
+
65
+ edge_index2 .append (torch .stack ([src_node2 .expand (dst_node2 .size (0 )), dst_node2 ], dim = 0 ))
66
+ edge_weight2 .append (src_node2_count .expand (dst_node2 .size (0 )) * dst_node2_count )
67
+
68
+ edge_index2 = torch .cat (edge_index2 , dim = 1 )
69
+ edge_weight2 = torch .cat (edge_weight2 , dim = 0 )
70
+
71
+ edge_index2 , edge_weight2 = coalesce (edge_index2 , edge_attr = edge_weight2 , num_nodes = node_sequence2 .size (0 ), reduce = "sum" )
72
+
73
+ data2 = Data (
74
+ edge_index = edge_index2 ,
75
+ num_nodes = node_sequence2 .size (0 ),
76
+ node_sequence = node_sequence2 ,
77
+ edge_weight = edge_weight2 ,
78
+ inverse_idx = edge1_to_node2 ,
79
+ )
80
+ layer2 = Graph (data2 )
81
+ layer2 .mapping = IndexMap (
82
+ [tuple (layer1 .mapping .to_ids (v .cpu ())) for v in node_sequence2 ]
83
+ )
84
+
85
+
86
+ m = MultiOrderModel ()
87
+ m .layers [1 ] = layer1
88
+ m .layers [2 ] = layer2
89
+ return m
0 commit comments