# 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()`

```
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 `PersistentGraph`

s. 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()`

```
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()`

```
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()`

```
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.