Skip to content

Views on a persistent graph

When dealing with the Graph object previously where edges are formed from instantaneous event streams, views were used to create a temporal bound on the graph to ultimately see how the graph changes over time. Single views were created using at, before, after and window, and iterators of windows were created using expanding and rolling. Functionality with the same name is available for the persistent graph. It shares similarities with the functionality for Graph but has some important differences. Through the use of example, this page aims to cover the behaviour of this time-bounding behaviour on the PersistentGraph.

Querying an instant of the graph with at()

Graph

G = PersistentGraph()

G.add_edge(2, "Alice", "Bob")
G.delete_edge(5, "Alice", "Bob")

# before the edge is added
print(f"At time 0: {G.at(0).nodes} {G.at(0).edges.explode()}")

# at the instant the edge is added
print(f"At time 2: {G.at(2).nodes} {G.at(2).edges.explode()}")

# while the graph is active
print(f"At time 3: {G.at(3).nodes} {G.at(3).edges.explode()}")

# the instant the edge is deleted
print(f"At time 5: {G.at(5).nodes} {G.at(5).edges.explode()}")

# after the edge is deleted
print(f"At time 6: {G.at(6).nodes} {G.at(6).edges.explode()}")

Output

At time 0: Nodes() Edges()
At time 2: Nodes(Node(name=Alice, earliest_time=2, latest_time=2), Node(name=Bob, earliest_time=2, latest_time=2)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=2, layer(s)=[_default]))
At time 3: Nodes(Node(name=Alice, earliest_time=3, latest_time=3), Node(name=Bob, earliest_time=3, latest_time=3)) Edges(Edge(source=Alice, target=Bob, earliest_time=3, latest_time=3, layer(s)=[_default]))
At time 5: Nodes(Node(name=Alice, earliest_time=5, latest_time=5), Node(name=Bob, earliest_time=5, latest_time=5)) Edges()
At time 6: Nodes(Node(name=Alice, earliest_time=6, latest_time=6), Node(name=Bob, earliest_time=6, latest_time=6)) Edges()

As we can see, the edge's presence in the graph is inclusive of the timestamp at which it was added, but exclusive of the timestamp at which it was deleted. You might think of it being present on a interval \(1 \leq t < 5 \subseteq \mathbb{Z}\). Note that the earliest and latest times for each edge is adjusted to the time bound in the query.

Another thing to note is that while nodes are not present until they are added (see example at time 1), but once they are added they are in the graph forever (see example at time 6). This differs from the Graph equivalent where nodes are present only when they contain an update within the time bounds. Crucially, this means that while performing a node count on a Graph will count the nodes who have activity (a property update, an adjacent edge added) within the time bounds specified, the same is not true for PersistentGraphs. As this may not be desirable, we are currently working on an alternative in which nodes without any edge within the time window will be treated as not present.

Getting the graph before a certain point with before()

Graph

G = PersistentGraph()

G.add_edge(2, "Alice", "Bob")
G.delete_edge(5, "Alice", "Bob")

# before the edge is added
print(f"Before time 1: {G.before(1).nodes} {G.before(1).edges.explode()}")

# at the instant the edge is added
print(f"Before time 2: {G.before(2).nodes} {G.before(2).edges.explode()}")

# while the graph is active
print(f"Before time 3: {G.before(3).nodes} {G.before(3).edges.explode()}")

# the instant the edge is deleted
print(f"Before time 5: {G.before(5).nodes} {G.before(5).edges.explode()}")

# after the edge is deleted
print(f"Before time 6: {G.before(6).nodes} {G.before(6).edges.explode()}")

Output

Before time 1: Nodes() Edges()
Before time 2: Nodes() Edges()
Before time 3: Nodes(Node(name=Alice, earliest_time=None, latest_time=2), Node(name=Bob, earliest_time=None, latest_time=2)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=2, layer(s)=[_default]))
Before time 5: Nodes(Node(name=Alice, earliest_time=None, latest_time=4), Node(name=Bob, earliest_time=None, latest_time=4)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=4, layer(s)=[_default]))
Before time 6: Nodes(Node(name=Alice, earliest_time=None, latest_time=5), Node(name=Bob, earliest_time=None, latest_time=5)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=5, layer(s)=[_default]))

Here we see that the before(T) bound is exclusive of the end point \(T\), creating an intersection between the time interval \(-\infty < t < T\) and \(2 \leq t < 5\) where \(T\) is the argument of before.

Getting the graph after a certain point with after()

Graph

G = PersistentGraph()

G.add_edge(2, "Alice", "Bob")
G.delete_edge(5, "Alice", "Bob")

# before the edge is added
print(f"After time 1: {G.after(1).nodes} {G.after(1).edges.explode()}")

# at the instant the edge is added
print(f"After time 2: {G.after(2).nodes} {G.after(2).edges.explode()}")

# while the graph is active
print(f"After time 3: {G.after(3).nodes} {G.after(3).edges.explode()}")

# the instant the edge is deleted
print(f"After time 5: {G.after(5).nodes} {G.after(5).edges.explode()}")

# after the edge is deleted
print(f"After time 6: {G.after(6).nodes} {G.after(6).edges.explode()}")

Output

After time 1: Nodes(Node(name=Alice, earliest_time=2, latest_time=9223372036854775806), Node(name=Bob, earliest_time=2, latest_time=9223372036854775806)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=5, layer(s)=[_default]))
After time 2: Nodes(Node(name=Alice, earliest_time=5, latest_time=9223372036854775806), Node(name=Bob, earliest_time=5, latest_time=9223372036854775806)) Edges(Edge(source=Alice, target=Bob, earliest_time=3, latest_time=5, layer(s)=[_default]))
After time 3: Nodes(Node(name=Alice, earliest_time=5, latest_time=9223372036854775806), Node(name=Bob, earliest_time=5, latest_time=9223372036854775806)) Edges(Edge(source=Alice, target=Bob, earliest_time=4, latest_time=5, layer(s)=[_default]))
After time 5: Nodes(Node(name=Alice, earliest_time=6, latest_time=9223372036854775806), Node(name=Bob, earliest_time=6, latest_time=9223372036854775806)) Edges()
After time 6: Nodes(Node(name=Alice, earliest_time=7, latest_time=9223372036854775806), Node(name=Bob, earliest_time=7, latest_time=9223372036854775806)) Edges()

after(T) is also exclusive of the starting point \(T\).

Windowing the graph with window()

Graph

G = PersistentGraph()

G.add_edge(2, "Alice", "Bob")
G.delete_edge(5, "Alice", "Bob")

# Touching the start time of the edge
print(f"Window 0,2: {G.window(0,2).nodes} {G.window(0,2).edges.explode()}")

# Overlapping the start of the edge
print(f"Window 0,4: {G.window(0,4).nodes} {G.window(0,4).edges.explode()}")

# Fully inside the edge time
print(f"Window 3,4: {G.window(3,4).nodes} {G.window(3,4).edges.explode()}")

# Touching the end of the edge
print(f"Window 5,8: {G.window(5,8).nodes} {G.window(5,8).edges.explode()}")

# Fully containing the edge
print(f"Window 1,8: {G.window(1,8).nodes} {G.window(1,8).edges.explode()}")

# after the edge is deleted
print(f"Window 6,10: {G.window(6,10).nodes} {G.window(6,10).edges.explode()}")

Output

Window 0,2: Nodes() Edges()
Window 0,4: Nodes(Node(name=Alice, earliest_time=None, latest_time=3), Node(name=Bob, earliest_time=None, latest_time=3)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=3, layer(s)=[_default]))
Window 3,4: Nodes(Node(name=Alice, earliest_time=3, latest_time=3), Node(name=Bob, earliest_time=3, latest_time=3)) Edges(Edge(source=Alice, target=Bob, earliest_time=3, latest_time=3, layer(s)=[_default]))
Window 5,8: Nodes(Node(name=Alice, earliest_time=5, latest_time=7), Node(name=Bob, earliest_time=5, latest_time=7)) Edges()
Window 1,8: Nodes(Node(name=Alice, earliest_time=None, latest_time=7), Node(name=Bob, earliest_time=None, latest_time=7)) Edges(Edge(source=Alice, target=Bob, earliest_time=2, latest_time=5, layer(s)=[_default]))
Window 6,10: Nodes(Node(name=Alice, earliest_time=6, latest_time=9), Node(name=Bob, earliest_time=6, latest_time=9)) Edges()

A window(T1, T2) creates a half-open interval \(T_1 \leq t < T_2\) intersecting the edge's active time ( \(2 \leq t < 5 \) in this case). Some examples to note are when the window is completely inside the edge active time and when the edge's active time is strictly inside the window. In both cases, the edge is treated as present in the graph. For some applications, it may be useful to consider edges whose last update inside the window is a deletion as being absent. This option is going to be explored in coming versions of Raphtory.