HigherOrderGraph / modified MultiOrderModel - #329
Conversation
… namespace); continuations method removed from EventGraph
…lable from package root
…legating to MultiOrderModel
M-Lampert
left a comment
There was a problem hiding this comment.
The PR looks good already. I made some comments; let me know if you have any questions. I think some of the questions that I raised should be discussed together with the others in our next meeting. I said so in the comments as well.
| def _validate_order(order: int) -> None: | ||
| """Reject orders for which no De Bruijn graph is defined.""" | ||
| if order < 1: | ||
| logger.error("order must be at least 1, got %s", order) | ||
| raise ValueError(f"order must be at least 1, got {order}") |
There was a problem hiding this comment.
There could actually be a zeroth-order De Bruijn graph and it is currently an open question whether that is something that should be included in the MultiOrderModel or not. See #172
This is probably something that we should discuss together in the next meeting if this is something that we want the HigherOrderModel to be able to represent or not.
| return IndexMap([tuple(v.tolist()) for v in node_sequence]) | ||
|
|
||
| @classmethod | ||
| def from_aggregated( |
There was a problem hiding this comment.
I think that name is confusing since it requires an unaggregated graph as input. So the function should either be named aggregate or from_unaggregated.
We could also think about giving the unaggregated graph a more consistent name throughout the whole repository. Currently, it doesn't have a distinct name since it is just the representation for a step in between with the final result being the higher-order DeBruijn Graph. Some naming suggestions:
- Higher-Order (path-)occurence graph
- Higher-Order line graph
- Lifted graph
If and how we name this intermediate unaggregated higher-order graph, is probably also something we should discuss in the next meeting.
| def lift(self, aggr: str = "src") -> HigherOrderGraph: | ||
| """Return the De Bruijn graph of order `k + 1` obtained by lifting this graph. | ||
|
|
||
| Nodes of the result are the edges of this graph, i.e. the paths of length `k + 1` | ||
| that exist in this graph's topology. | ||
|
|
||
| Args: | ||
| aggr: Aggregation used for the lifted edge weights. One of "src", "dst", | ||
| "max", "mul" or "add". | ||
|
|
||
| Returns: | ||
| HigherOrderGraph: A higher-order graph of order `k + 1`. | ||
| """ | ||
| edge_index = self.data.edge_index.as_tensor() | ||
| if "edge_weight" in self.data: | ||
| edge_weight = self.data.edge_weight | ||
| else: | ||
| edge_weight = torch.ones(edge_index.size(1), device=edge_index.device) | ||
|
|
||
| ho_index, node_sequence, ho_weight = lift_order_step( | ||
| edge_index, self.data.node_sequence, edge_weight=edge_weight, aggr=aggr | ||
| ) |
There was a problem hiding this comment.
I think this lift should use the pre-aggregation representation rather than self.data.edge_index.
At this point, edge_index and edge_weight have already been coalesced by from_aggregated(). Lifting them constructs paths implied by the aggregated De Bruijn topology, which can combine occurrences that were never observed consecutively in the source data and can propagate already-aggregated weights incorrectly.
inverse_idx already lets us reconstruct the pre-aggregation node sequences via:
node_sequence = self.data.node_sequence[self.data.inverse_idx]If from_aggregated() also retains the occurrence-level edge index and weights (e.g. pre_aggregation_edge_index and pre_aggregation_edge_weight), then .lift() can call lift_order_step() on those tensors and aggregate only the result. That would make h.lift() consistent with the unaggregated iteration used by MultiOrderModel.
| m.layers[1] = HigherOrderGraph.from_aggregated_graph( | ||
| g1, first_order_mapping=path_data.mapping, n_first_order=n_first_order | ||
| ) |
There was a problem hiding this comment.
Should we check if cached or max_order == 1: here?
| - `node_sequence`: Node sequence [tensor][torch.Tensor] of shape `(num_nodes, order)` where each entry | ||
| corresponds to the index of first-order nodes in the underlying graph and mapping. For first-order graphs, | ||
| the indices in the node sequence is identical to the indices in the edge index. For higher-order graphs, | ||
| the node sequence contains tuples of node indices representing higher-order nodes that correspond to paths in | ||
| the underlying first-order graph. |
There was a problem hiding this comment.
Since we have a dedicated higher-order graph class now, we can remove the higher-order workarounds in Graph like the node_sequence attribute that is only needed for orders larger than 1.
| # For higher-order graphs, we need to update the inverse_idx attribute | ||
| if "inverse_idx" in d: | ||
| d.inverse_idx = mapping.to_idxs( | ||
| np.concatenate([m1.to_ids(d1.inverse_idx), m2.to_ids(d2.inverse_idx)]), | ||
| device=d.inverse_idx.device, | ||
| ) |
There was a problem hiding this comment.
This is also something only necessary for higher-order graphs and can be removed now.
| @property | ||
| def order(self) -> int: | ||
| """Return order of graph. | ||
|
|
||
| Returns: | ||
| int: order of the (De Bruijn) graph | ||
| """ | ||
| return self.data.node_sequence.size(1) |
There was a problem hiding this comment.
From now on, Graph is exclusively used for order-1 graphs, so this property is not necessary anymore.
|
|
||
| Each higher-order node is replaced by one of the first-order nodes of its path, | ||
| and the weights of higher-order edges mapping to the same first-order edge are | ||
| summed. First-order nodes not traversed by any path remain as isolated nodes. |
There was a problem hiding this comment.
| summed. First-order nodes not traversed by any path remain as isolated nodes. | |
| summed. First-order nodes not traversed by any path remain as isolated nodes. | |
| Warning: This is a projection, not an inverse transformation | |
| This method does not reconstruct the original first-order graph from | |
| which this higher-order graph was built. Instead, it maps each higher- | |
| order node to either the first or last first-order node in its represented path. | |
| Consequently, the result preserves flow encoded by the higher-order model | |
| under the selected projection, but may differ from the original graph in its | |
| edge set. In particular, isolated first-order edges cannot be recovered. |
A
HigherOrderGraphclass of arbitrary order, constructable from temporal_graph/event_graph/path_data. It delegates most of the work to theMultiOrderModelclass which does the iterations on the lifting (withcached=False). It is also constructable usingfrom_aggregated, and has its own.liftmethod.The
MultiOrderModelclass now hasHigherOrderGraphs in its layers.None of the tests for
MultiOrderModelneeded modifications and still pass, which is reassuring. Tests forHigherOrderModelandMultiOrderModelthat assumeHigherOrderModelin layers are coming next.This PR assumes that the
EventGraphbranch is merged, as it builds on top of it.Typical workflow using these new classes: