Skip to content

relationalai.std.graphs.Compute

class relationalai.std.graphs.Compute

The Compute class serves as a namespace for various graph algorithms. This class is automatically instantiated when you create a Graph object and is accessible via the graph’s .compute attribute. It provides methods for computing basic graph statistics, centrality and similarity measures, community detection, and more.

Graph algorithms are executed by calling the appropriate method from a Graph object’s .compute attribute. The following example demonstrates how to compute the PageRank of each person in a social network graph:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.friends.extend([bob, carol])
carol.friends.add(daniel)
# Create an undirected graph with Person nodes and edges from people to their friends.
# This graph has three edges: one from Alice to Bob, Alice to Carol, and Carol to Daniel.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
with model.query() as select:
# Get all person objects.
person = Person()
# Compute the PageRank of each person.
pagerank = graph.compute.pagerank(person)
# Select the each person's name and their PageRank value.
response = select(person.name, alias(pagerank, "pagerank"))
print(response.results)
# Output:
# name pagerank
# 0 Alice 0.324562
# 1 Bob 0.175438
# 2 Carol 0.324562
# 3 Daniel 0.175438

See the documentation for each method in the Methods section for more examples.

Compute.num_nodes() -> Expression

Get the number of nodes in the graph. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes

Returns an Expression that produces the number of nodes in the graph as an integer value.

Use .num_nodes() to get the number of nodes in a graph. You access the .num_nodes() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the graph and connect them with a 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
alice.friends.add(bob)
bob.friends.add(alice)
# Create an undirected graph with Person nodes and edges between friends.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Get the number of nodes in the graph.
with model.query() as select:
num_nodes = graph.compute.num_nodes()
response = select(alias(num_nodes, "num_nodes"))
print(response.results)
# Output:
# num_nodes
# 0 2
Compute.num_edges() -> Expression

Get the number of edges in the graph. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes

Returns an Expression that produces the number of edges in the graph as an integer value.

Use .num_edges() to get the number of edges in a graph. You access the .num_edges() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the graph and connect them with a 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
alice.friends.add(bob)
bob.friends.add(alice)
# Create an undirected graph with Person nodes and edges between friends.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Get the number of edges in the graph.
with model.query() as select:
num_edges = graph.compute.num_edges()
response = select(alias(num_edges, "num_edges"))
print(response.results)
# Output:
# num_edges
# 0 1
Compute.degree(node: Producer) -> Expression

Compute the degree of a node. For an undirected graph, the degree of a node is the number of neighbors it has in the graph. In a directed graph, the degree of a node is the sum of its indegree and outdegree. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesA keyword argument determines if weights are used in this computation.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.
weightNone or strOptional; If weight, the degree is computed as the sum of the weights of the edges incident to the node.

Returns an Expression object that produces the degree of the node as an integer value.

Use .degree() to compute the degree of a node in a graph. You access the .degree() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the graph and connect them with a 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
alice.friends.add(bob)
bob.friends.add(alice)
# Create an undirected graph with Person nodes and edges between friends.
# This graph has one edge between Alice and Bob.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Get the number of nodes in the graph.
with model.query() as select:
# Get all Person objects.
person = Person()
# Compute the degree of each person.
degree = graph.compute.degree(person)
# Select the name of each person and their degree.
response = select(person.name, degree)
print(response.results)
# Output:
# name v
# 0 Alice 1
# 1 Bob 1

In directed graphs, the degree of a node is the sum of its indegree and outdegree:

# Create a directed graph with Person nodes and edges between friends.
# Note that graphs are directed by default.
# This graph has two edges. One from Alice to Bob and one from Bob to Alice.
directed_graph = Graph(model)
directed_graph.Node.extend(Person)
directed_graph.Edge.extend(Person.friends)
# Get the degree of each person in the graph.
with model.query() as select:
person = Person()
degree = directed_graph.compute.degree(person)
response = select(person.name, degree)
print(response.results)
# Output:
# name v
# 0 Alice 2
# 1 Bob 2
Compute.indegree(node: Producer) -> Expression

Compute the indegree of a node. In a directed graph, the indegree of a node is the number of edges that point to the node. For an undirected graph, .indegree() is an alias of .degree(). Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the indegree of the node as an integer value.

Use .indegree() to compute the indegree of a node in a graph. You access the .indegree() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the graph and connect them with a 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
alice.friends.add(bob)
bob.friends.add(alice)
# Create a directed graph with Person nodes and edges between friends.
# Note that graphs are directed by default.
# This graphs has two edges: one from Alice to Bob and one from Bob to Alice.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Get the number of nodes in the graph.
with model.query() as select:
# Get all Person objects.
person = Person()
# Compute the indegree of each person.
indegree = graph.compute.indegree(person)
# Select the name of each person and their indegree.
response = select(person.name, indegree)
print(response.results)
# Output:
# name v
# 0 Alice 1
# 1 Bob 1

For an undirected graph, .indegree() is same as .degree().

Compute.outdegree(node: Producer) -> Expression

Compute the outdegree of a node in the graph. In a directed graph, the outdegree of a node is the number of edges that point away from the node. For an undirected graph, .outdegree() is an alias of .degree(). Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the outdegree of the node as an integer value.

Use .outdegree() to compute the outdegree of a node in a graph. You access the .outdegree() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the graph and connect them with a 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
alice.friends.add(bob)
bob.friends.add(alice)
# Create a directed graph with Person nodes and edges between friends.
# Note that graphs are directed by default.
# This graphs has two edges: one from Alice to Bob and one from Bob to Alice.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Get the number of nodes in the graph.
with model.query() as select:
# Get all Person objects.
person = Person()
# Compute the outdegree of each person.
outdegree = graph.compute.outdegree(person)
# Select the name of each person and their outdegree.
response = select(person.name, outdegree)
print(response.results)
# Output:
# name v
# 0 Alice 1
# 1 Bob 1

In undirected graphs, .outdegree() is same as .degree().

Compute.betweenness_centrality(node: Producer) -> Expression

Computes the betweenness centrality of a node. Betweenness centrality measures the importance of a node by counting how often it appears on the shortest paths between pairs of nodes in a graph. Nodes with high betweenness centrality may play critical roles in the flow of information or resources through a network. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesEdge direction is respected during shortest path computations.
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYesShortest paths are computed assuming all edges have equal weight.
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the betweenness centrality of the node as a floating-point value.

Betweenness centrality is a measure of a node’s importance based on its role as a bridge in the network. Specifically, it quantifies how often a node lies on the shortest paths between other pairs of nodes. Nodes with high betweenness centrality have significant influence over the flow of information, resources, or interactions in the network. This makes the metric especially relevant in fields like sociology (to identify brokers or gatekeepers), transportation (to detect critical transit points), and biology (to pinpoint bottleneck molecules in interaction networks). Formally, the betweenness centrality of a node in a graph is defined as:

where is the total number of shortest paths between nodes and , and is the number of those paths that pass through . While more computationally intensive than simpler centrality metrics, betweenness centrality captures global structural importance and is highly sensitive to network topology.

Calculating betweenness centrality requires determining all shortest paths between each pair of nodes within a graph, which makes it computationally expensive to compute on large graphs. To keep the cost manageable, betweenness_centrality() gives an approximation using the Brandes-Pich algorithm, which samples nodes uniformly at random and performs single-source shortest-path computations from those nodes. This implementation nominally samples 100 nodes uniformly at random, yielding time complexity of 100 * O(|V|+|E|) where |V| and |E| are the number of nodes and edges respectively. If the graph has fewer than 100 nodes, the implementation reduces to the exact Brandes algorithm, with time complexity O(|V|(|V|+|E|)) for unweighted graphs.

Use .betweenness_centrality() to compute the betweenness centrality of a node in a graph. You access the .betweenness_centrality() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.extend([bob, carol])
bob.follows.add(alice)
carol.follows.add(alice)
# Create a directed graph with Person nodes and edge from people to the people they follow.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Compute the betweenness centrality of each person in the graph.
with model.query() as select:
# Get all person objects.
person = Person()
# Compute the betweenness centrality of each person.
centrality = graph.compute.betweenness_centrality(person)
# Select the each person's name and their betweenness centrality.
response = select(person.name, alias(centrality, "betweenness_centrality"))
print(response.results)
# Output:
# name betweenness_centrality
# 0 Alice 2.0
# 1 Bob 0.0
# 2 Carol 0.0

If one of the objects produced by the node producer is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Get the betweenness centrality of each person and company in the graph.
with model.query() as select:
# Get all person and company objects.
obj = (Person | Company)()
# Compute the betweenness centrality of each object.
# Objects that are not nodes in the graph are filtered out.
centrality = graph.compute.betweenness_centrality(obj)
# Select the each object's name and their betweenness centrality.
response = select(obj.name, alias(centrality, "betweenness_centrality"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name betweenness_centrality
# 0 Alice 2.0
# 1 Bob 0.0
# 2 Carol 0.0
Compute.degree_centrality(node: Producer) -> Expression

This algorithm computes the degree centrality of a node. For unweighted graphs, it measures the importance of a node based on its degree. Nodes with high degree centrality are well-connected in the graph.

For weighted graphs, the weighted degree centrality of a node is the sum of the weights of the edges incident to the node divided by one less than the number of nodes in the graph.

Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesDegree is defined as the sum of in-degree and out-degree.
UndirectedYesDegree is defined as usual.
WeightedYesWeights must be greater than or equal to 0.0; values are summed and normalized.
UnweightedYesResults are computed using a weight of 1.0 for each edge.
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the degree centrality of the node as a floating-point value, calculated by the following formula:

degree centrality = degree of the node / (total number of nodes - 1)

This value is at most 1.0 for simple graphs with no self loops. In graphs with self loops, a node’s degree centrality can exceed 1.0.

Degree centrality quantifies the influence of a node based solely on the number of connections it has. In undirected graphs, this corresponds to the number of adjacent edges, while in directed graphs, one can distinguish between in-degree (incoming edges) and out-degree (outgoing edges). Nodes with high degree centrality are often hubs or key connectors in networks, making this measure particularly useful in social network analysis, epidemiology (e.g., identifying super-spreaders), and infrastructure design.

Mathematically, the degree centrality of a node in a graph is defined as:

where is the degree of node (the number of edges incident to it) and is the total number of nodes in the graph.

Use .degree_centrality() to compute the degree centrality of a node in a graph. You access the .degree_centrality() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.extend([bob, carol])
bob.follows.add(alice)
carol.follows.add(alice)
# Create a directed graph with Person nodes and edge from people to the people they follow.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Compute the degree centrality of each person in the graph.
with model.query() as select:
# Get all person objects.
person = Person()
# Compute the degree centrality of each person.
centrality = graph.compute.degree_centrality(person)
# Select the each person's name and their degree centrality.
response = select(person.name, alias(centrality, "degree_centrality"))
print(response.results)
# Output:
# name degree_centrality
# 0 Alice 2.0
# 1 Bob 1.0
# 2 Carol 1.0

If one of the objects produced by the node producer is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Get the degree centrality of each person and company in the graph.
with model.query() as select:
# Get all person and company objects.
obj = (Person | Company)()
# Compute the degree centrality of each object.
# Objects that are not nodes in the graph are filtered out.
centrality = graph.compute.degree_centrality(obj)
# Select the each object's name and their degree centrality.
response = select(obj.name, alias(centrality, "degree_centrality"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name degree_centrality
# 0 Alice 2.0
# 1 Bob 1.0
# 2 Carol 1.0

Use .degree_centrality() to compute the weighted degree centrality of a node in a graph. You access the .degree_centrality() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with Person and Friendship types.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
Friendship = model.Type("Friendship")
# Add some people to the model and connect them with friendships.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
Friendship.add(person1=alice, person2=bob, strength=100)
Friendship.add(person1=bob, person2=carol, strength=10)
# Create an weighted, undirected graph with Person nodes and edges between friends.
# This graph has two edges: one from Alice and Bob and one from Bob and Carol.
# The edges are weighted by the strength of each friendship.
graph = Graph(model, undirected=True, weighted=True)
graph.Node.extend(Person)
with model.rule():
friendship = Friendship()
graph.Edge.add(friendship.person1, friendship.person2, weight=friendship.strength)
# Compute the weighted degree centrality of each person in the graph.
with model.query() as select:
person = Person()
centrality = graph.compute.degree_centrality(person)
response = select(person.name, alias(centrality, "degree_centrality"))
print(response.results)
# Output:
# name degree_centrality
# 0 Alice 50.0
# 1 Bob 55.0
# 2 Carol 5.0
Compute.eigenvector_centrality(node: Producer) -> Expression

Computes the eigenvector centrality of a node in an undirected graph. Eigenvector centrality measures a node’s significance in a graph, taking into account not only its direct connections, but also the centrality of those connected nodes. Specifically, it assigns to each node a centrality score that is proportional to the sum of the centrality scores of its neighboring nodes. Thus, nodes with high eigenvector centrality are important because they are connected to other important nodes. Must be called in a rule or query context.

Directed graphs are not currently supported. See .pagerank() for a similar measure that works with directed graphs.

Graph TypeSupportedNotes
DirectedNo
UndirectedYesSee Returns for convergence criteria.
WeightedYesWeights must be greater than or equal to zero. Edges with larger weights are more likely to be traversed.
UnweightedYesResults are computed using a weight of 1.0 for each edge.
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the eigenvector centrality of the node as a floating-point value.

We use the power iteration method, which converges when there is a unique eigenvalue with the largest absolute magnitude. An important example where this condition does not hold is in the case of bipartite graphs. Specifically, in a bipartite graph, the adjacency spectrum has a unique property: its eigenvalues are symmetric around zero, meaning for every positive eigenvalue, there is a corresponding negative eigenvalue of the same magnitude.

Eigenvector centrality is a measure of node importance that accounts not just for how many connections a node has, but how well-connected its neighbors are. Unlike degree centrality, which treats all neighbors equally, eigenvector centrality assigns higher scores to nodes that are connected to other influential nodes, capturing recursive influence. It is widely used in social network analysis, web ranking (e.g., PageRank is a variant), and biological networks to identify central or influential actors in a network structure.

Mathematically, the centrality score of each node , is defined to be proportional to the sum of the scores of its neighbors, leading to the eigenvector equation , where is the adjacency matrix and is the centrality vector. To compute this, we use the power iteration method, which converges to the principal eigenvector associated with the largest eigenvalue in magnitude—assuming this eigenvalue is unique. However, in bipartite graphs, the adjacency matrix has a symmetric spectrum: for every eigenvalue , is also an eigenvalue. This symmetry can lead to convergence issues, as there may not be a dominant eigenvalue in magnitude. In such cases, the power method can oscillate or converge to a non-informative solution

Use .eigenvector_centrality() to compute the eigenvector centrality of a node in an undirected graph. Directed graphs are currently not supported.

You access the .eigenvector_centrality() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.friends.extend([bob, carol])
carol.friends.add(daniel)
# Create an undirected graph with Person nodes and edges from people to their friends.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
with model.query() as select:
# Get all person objects.
person = Person()
# Compute the eigenvector centrality of each person.
centrality = graph.compute.eigenvector_centrality(person)
# Select the each person's name and their eigenvector centrality.
response = select(person.name, alias(centrality, "eigenvector_centrality"))
print(response.results)
# Output:
# name eigenvector_centrality
# 0 Alice 0.601501
# 1 Bob 0.371748
# 2 Carol 0.601501
# 3 Daniel 0.371748

If one of the objects produced by the node producer is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
with model.query() as select:
# Get all person and company objects.
obj = (Person | Company)()
# Compute the eigenvector centrality of each object.
# Objects that are not nodes in the graph are filtered out.
centrality = graph.compute.eigenvector_centrality(obj)
# Select the each object's name and their eigenvector centrality.
response = select(obj.name, alias(centrality, "eigenvector_centrality"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name eigenvector_centrality
# 0 Alice 0.601501
# 1 Bob 0.371748
# 2 Carol 0.601501
# 3 Daniel 0.371748
Compute.pagerank(
node: Producer,
damping_factor: float = 0.85,
tolerance: float = 1e-6,
max_iter: int = 20
) -> Expression

Computes the PageRank centrality of a node in a graph. PageRank measures a node’s influence in a graph based on the quality and quantity of its inbound links. Nodes with high PageRank values may be considered more influential than other nodes in the network. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesEdge direction is used to guide the random walk.
UndirectedYesThe random walk can traverse edges in any direction.
WeightedYesWeights must be greater than or equal to 0.0. Edges with larger weights are more likely to be traversed.
UnweightedYesResults are computed using a weight of 1.0 for each edge.
NameTypeDescriptionRange
nodeProducerA node in the graph.Vertex set.
damping_factorfloatThe PageRank damping factor. Must be between 0.0 and 1.0, exclusive of 1.0. Default is 0.85.[0,1).
tolerancefloatThe convergence tolerance for the PageRank algorithm. Default is 1e-6.Small positive number or zero.
max_iterintThe maximum number of iterations allowed in the PageRank algorithm. Default is 20.Positive integer.

Returns an Expression object that produces the PageRank centrality of the node as a floating-point value.

PageRank measures the importance of nodes based on the structure of inbound connections. Originally developed by Google for ranking webpages, it assigns higher scores to nodes that are linked to by other high-scoring nodes. The method models a random walk through the graph: at each step, the walk either follows an outgoing edge or jumps to a random node, with a probability governed by the damping factor. Nodes with high PageRank are considered more “influential” or “visible” within the network, making this metric useful in domains such as web ranking, citation analysis, and social network influence detection.

This implementation supports directed, undirected, weighted, and unweighted graphs. For weighted graphs, only non-negative weights are allowed, and weights influence the likelihood of traversing particular edges during the random walk. The algorithm uses the power iteration method to approximate the stationary distribution of the walk, terminating when the change in scores across iterations falls below a user-specified tolerance (default value of 0.000001), or when the number of iterations exceeds max_iter (default value of 20). The damping_factor (default value of 0.85) controls the probability of continuing the walk versus jumping randomly. The result is a real-valued centrality score for each node.

Use .pagerank() to compute the PageRank centrality of a node in a graph.

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued 'friends' property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.friends.extend([bob, carol])
carol.friends.add(daniel)
# Create an undirected graph with Person nodes and edges from people to their friends.
# This graph has three edges: one from Alice to Bob, Alice to Carol, and Carol to Daniel.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
with model.query() as select:
# Get all person objects.
person = Person()
# Compute the PageRank of each person.
pagerank = graph.compute.pagerank(person)
# Select the each person's name and their PageRank value.
response = select(person.name, alias(pagerank, "pagerank"))
print(response.results)
# Output:
# name pagerank
# 0 Alice 0.324562
# 1 Bob 0.175438
# 2 Carol 0.324562
# 3 Daniel 0.175438

If one of the objects produced by the node producer is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
with model.query() as select:
# Get all person and company objects.
obj = (Person | Company)()
# Compute the PageRank of each object.
# Objects that are not nodes in the graph are filtered out.
pagerank = graph.compute.pagerank(obj)
# Select the each object's name and their PageRank value.
response = select(obj.name, alias(pagerank, "pagerank"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name pagerank
# 0 Alice 0.324562
# 1 Bob 0.175438
# 2 Carol 0.324562
# 3 Daniel 0.175438
Compute.ego_network(node: Producer,
hops: int = 1
) -> Expression

This method returns the induced subgraph containing all nodes within k undirected hops from a specified source node (ego-node). It identifies nearby nodes based on undirected shortest path distance, effectively capturing the local structure around the ego-node. This is useful for analyzing local connectivity patterns or extracting ego-networks. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesEdge direction is ignored; traversal is based on undirected distance.
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes

Returns an Expression that produces the set of edges induced by the ego_network function.

The .ego_network() method extracts the local neighborhood surrounding a given node (the ego-node) by returning the edges in the induced subgraph of all nodes reachable within a specified number of undirected hops. This is known as the ego network and is commonly used to study localized structure, such as social circles, immediate influence zones, or local clustering. By default, the function returns the 1-hop neighborhood, which includes the ego-node, its neighbors, and all edges among them.

The function operates on the undirected version of the graph, regardless of whether the input graph is directed or undirected. It also supports weighted and unweighted graphs, but ignores edge weights and instead uses hop-based distance. The parameter hops controls the radius of the neighborhood: for example, hops=2 returns all nodes reachable from the ego-node in up to two undirected steps. The returned value is an expression producing the set of edges in the resulting subgraph.

You access the .ego_network(node, hops) method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
model = rai.Model("socialNetwork")
Person = model.Type("Person")
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
claire = Person.add(name="Claire")
dave = Person.add(name="Dave")
eve = Person.add(name="Eve")
frank = Person.add(name="Frank")
gina = Person.add(name="Gina")
alice.follows.add(alice)
alice.follows.add(bob)
bob.follows.add(claire)
claire.follows.add(dave)
dave.follows.add(eve)
eve.follows.add(frank)
frank.follows.add(gina)
graph = Graph(model, undirected=False)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
with model.query() as select:
node = Person()
a,b = graph.compute.ego_network(node, 1)
ego_response = select(alias(node.name, "ego-node") , alias( Person(a).name, "u"), alias(Person(b).name, "v"))
print(ego_response.results)
# output
# ego-node u v
# 0 Alice Alice Alice
# 1 Alice Alice Bob
# 2 Bob Alice Alice
# 3 Bob Alice Bob
# 4 Bob Bob Claire
# 5 Claire Bob Claire
# 6 Claire Claire Dave
# 7 Dave Claire Dave
# 8 Dave Dave Eve
# 9 Eve Dave Eve
# 10 Eve Eve Frank
# 11 Frank Eve Frank
# 12 Frank Frank Gina
# 13 Gina Frank Gina

If you’re interested in retrieving the 2-hop ego-network of a specific node (e.g., Alice), you can apply the following modification to the code above:

with model.query() as select:
node = Person(name="Alice")
a,b = graph.compute.ego_network(node, hops=2)
ego_response = select(alias(node.name, "ego-node") , alias( Person(a).name, "u"), alias(Person(b).name, "v"))
# ego-node u v
# 0 Alice Alice Alice
# 1 Alice Alice Bob
# 2 Alice Bob Claire
Compute.cosine_similarity(node1: Producer, node2: Producer) -> Expression

This algorithm measures the cosine similarity between two nodes in a graph.

For unweighted graphs, it measures the similarity between two nodes based on the inner product between respective neighborhood vectors. Values range from 0.0 to 1.0, inclusive, where 1.0 indicating that the nodes have identical neighborhoods and 0.0 indicating no meaningful relationship.

For weighted graphs, it measures the similarity between two nodes based on the inner product between respective neighborhood vectors, weighted according to the edges. Values range from -1.0 to 1.0, inclusive, with higher value indicating greater similarity. If all weights are 1.0 it degenerates to the unweighted case.

In both cases, pairs of nodes with a similarity of 0.0, indicating no meaningful relationship, are excluded from results for improved performance.

Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesBased on out-neighbors.
UndirectedYes
WeightedYesEdge weights are used as vector entries. All weights must be numeric; negative weights are allowed but may produce values in .
UnweightedYesTreated as binary neighbor vectors.
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object that produces the cosine similarity between the two nodes as a floating-point value.

Cosine similarity is a structural measure of similarity between two nodes based on their neighborhood overlap. In unweighted graphs, it computes the cosine of the angle between binary vectors representing each node’s neighbors (outneighbors for directed graphs)—yielding a value between 0.0 and 1.0. A score of 1.0 indicates that the nodes have identical neighbors, while 0.0 means they share none. In weighted graphs, the same idea applies, but the vectors encode edge weights, allowing for a richer measure of proximity. This algorithm supports both unweighted and weighted graphs, and by default excludes node pairs with a similarity of 0.0 for performance.

Formally, this algorithm computes the standard formula for cosine similarity. Given two nodes and , let their (possibly weighted) neighborhood vectors be ​ and ​. Cosine similarity is defined as:

where is the dot product of the vectors, and and are their respective norms (magnitudes). For unweighted graphs, these vectors are binary, and the dot product counts the number of common neighbors. For weighted graphs, the dot product and norms incorporate edge weights, resulting in a score that may range from −1.0 to 1.0, although negative values are rare in most real-world graph applications. If all edge weights are 1.0, the algorithm behaves identically to the unweighted case. By default, node pairs with a similarity score of 0.0 are omitted from the results.

Use .cosine_similarity() to compute the cosine similarity between two nodes in a graph.

You access the .cosine_similarity() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.friends.add(bob)
bob.friends.add(carol)
# Create an undirected graph with Person nodes and edges between friends.
# Note that cosine similarity is only supported for undirected graphs.
# This graph has two edges: one between Alice and Bob, and one between Bob and Carol.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
with model.query() as select:
# Get pairs of people.
person1, person2 = Person(), Person()
# Compute the cosine similarity between each pair of people.
similarity = graph.compute.cosine_similarity(person1, person2)
# Select each person's name and their similarity value.
response = select(person1.name, person2.name, alias(similarity, "cosine_similarity"))
print(response.results)
# Output:
# name name2 cosine_similarity
# 0 Alice Alice 1.0
# 1 Alice Carol 1.0
# 2 Bob Bob 1.0
# 3 Carol Alice 1.0
# 4 Carol Carol 1.0

There is no row for Alice and Bob in the preceding query’s results. That’s because Alice and Bob have a cosine similarity of 0.0. Pairs of nodes with zero similarity, indicating no meaningful similarity, are often excluded from analyses. Consequently, we filter out these pairs to improve performance.

If node1 or node2 is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Create the union of the Person and Company types.
PersonOrCompany = Person | Company
with model.query() as select:
# Get all person and company objects.
obj1, obj2 = PersonOrCompany(), PersonOrCompany()
obj1 < ob2 # Ensure pairs are unique. Compares internal object IDs.
# Compute the cosine similarity between each pair of objects.
# Objects that are not nodes in the graph are filtered out of the results.
similarity = graph.compute.cosine_similarity(obj1, obj2)
response = select(obj1.name, obj2.name, alias(similarity, "cosine_similarity"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name name2 cosine_similarity
# 0 Carol Alice 1.0

Use .cosine_similarity() to compute the weighted cosine similarity between two nodes in a graph.

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with Person and Friendship types.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
Friendship = model.Type("Friendship")
# Add some people to the model and connect them with friendships.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
Friendship.add(person1=alice, person2=bob, strength=20)
Friendship.add(person1=bob, person2=carol, strength=10)
Friendship.add(person1=alice, person2=carol, strength=10)
# Create a weighted, undirected graph with Person nodes and edges between friends.
# This graph has two edges: one between Alice and Bob, and one between Bob and Carol.
# The edges are weighted by the strength of each friendship.
graph = Graph(model, undirected=True, weighted=True)
graph.Node.extend(Person)
with model.rule():
friendship = Friendship()
graph.Edge.add(friendship.person1, friendship.person2, weight=friendship.strength)
# Compute the weighted cosine similarity between each pair of people in the graph.
with model.query() as select:
person1, person2 = Person(), Person()
similarity = graph.compute.cosine_similarity(person1, person2)
response = select(person1.name, person2.name, alias(similarity, "cosine_similarity"))
print(response.results)
# Output:
# name name2 cosine_similarity
# 0 Alice Alice 1.000000
# 1 Alice Bob 0.200000
# 2 Alice Carol 0.894427
# 3 Bob Alice 0.200000
# 4 Bob Bob 1.000000
# 5 Bob Carol 0.894427
# 6 Carol Alice 0.894427
# 7 Carol Bob 0.894427
# 8 Carol Carol 1.000000
Compute.jaccard_similarity(node1: Producer, node2: Producer) -> Expression

This algorithm measures the Jaccard similarity between two nodes in a graph.

For unweighted graphs, it measures the similarity between two nodes based on how many neighbors (or out-neighbors if the graph is directed) they share. Values range from 0.0 to 1.0, inclusive, with 1.0 indicating that the nodes have identical neighborhoods and 0.0 indicating no meaningful relationship.

For weighted graphs, it measures the similarity between two nodes as the ratio of the sums of the minimum and maximum edge weights connecting them. Values range from 0.0 to 1.0, inclusive, with higher values indicating greater similarity. If all weights are 1.0 it degenerates to the unweighted case.

In both cases, pairs of nodes with a similarity of 0.0, indicating no meaningful relationship, are excluded from results for improved performance.

Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesBased on out-neighbors.
UndirectedYes
WeightedYes
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object.

Jaccard similarity quantifies the similarity between two nodes based on their neighborhood overlap. It is particularly valuable in social networks, recommendation systems, and link prediction tasks, where understanding the degree of shared connections between nodes can reveal underlying relationships or suggest potential future connections. In an undirected graph, the Jaccard similarity between two vertices and is defined as the size of the intersection of their neighbor sets divided by the size of the union. This ratio provides an interpretable score between 0 and 1, where a higher value indicates greater similarity in connectivity patterns.

Formally, for a graph , let and denote the sets of neighbors of vertices and , respectively. Then the Jaccard similarity between and is given by:

where counts how many neighbors and have in common, and counts the total number of distinct neighbors they have. The measure is undefined if both and are empty, though in practice this is typically handled by returning a similarity of 0. For directed graphs, the measure is based on out-neighbors, meaning it considers only the neighbors that can be reached by following outgoing edges. The algorithm supports both unweighted and weighted graphs.

Use .jaccard_similarity() to compute the Jaccard similarity between two nodes in a graph. You access the .jaccard_similarity() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.friends.add(bob)
bob.friends.add(carol)
# Create an undirected graph with Person nodes and edges between friends.
# This graph has two edges: one between Alice and Bob, and one between Bob and Carol.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
with model.query() as select:
# Get pairs of people.
person1, person2 = Person(), Person()
# Compute the Jaccard similarity between each pair of people.
similarity = graph.compute.jaccard_similarity(person1, person2)
# Select each person's name and their similarity value.
response = select(person1.name, person2.name, alias(similarity, "jaccard_similarity"))
print(response.results)
# Output:
# name name2 jaccard_similarity
# 0 Alice Alice 1.0
# 1 Alice Carol 1.0
# 2 Bob Bob 1.0
# 3 Carol Alice 1.0
# 4 Carol Carol 1.0

There is no row for Alice and Bob in the preceding query’s results. That’s because Alice and Bob have a Jaccard similarity of 0.0. Pairs of nodes with zero similarity, indicating no meaningful similarity, are often excluded from analyses. Consequently, we filter out these pairs to improve performance.

If node1 or node2 is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Create the union of the Person and Company types.
PersonOrCompany = Person | Company
with model.query() as select:
# Get all person and company objects.
obj1, obj2 = PersonOrCompany(), PersonOrCompany()
obj1 < obj2 # Ensure pairs are unique. Compares internal object IDs.
# Compute the Jaccard similarity between each pair of objects.
# Objects that are not nodes in the graph are filtered out of the results.
similarity = graph.compute.jaccard_similarity(obj1, obj2)
response = select(obj1.name, obj2.name, alias(similarity, "jaccard_similarity"))
# Only rows for people are returned, since companies are not nodes in the graph.
# Also note that unlike in the preceding case, only distinct pairs of people are returned. We ensure this by requiring obj1 < obj2,
# where obj1 and obj2 represent the internal IDs of the individuals, and the IDs are ordered numerically.
print(response.results)
# Output:
# name name2 jaccard_similarity
# 0 Carol Alice 1.0

Use .jaccard_similarity() to compute the weighted Jaccard similarity between two nodes in a graph.

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with Person and Friendship types.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
Friendship = model.Type("Friendship")
# Add some people to the model and connect them with friendships.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
Friendship.add(person1=alice, person2=bob, strength=20)
Friendship.add(person1=bob, person2=carol, strength=10)
Friendship.add(person1=alice, person2=carol, strength=10)
# Create a weighted, undirected graph with Person nodes and edges between friends.
# This graph has two edges: one between Alice and Bob, and one between Bob and Carol.
# The edges are weighted by the strength of each friendship.
graph = Graph(model, undirected=True, weighted=True)
graph.Node.extend(Person)
with model.rule():
friendship = Friendship()
graph.Edge.add(friendship.person1, friendship.person2, weight=friendship.strength)
# Compute the weighted Jaccard similarity between each pair of people in the graph.
with model.query() as select:
person1, person2 = Person(), Person()
similarity = graph.compute.jaccard_similarity(person1, person2)
response = select(person1.name, person2.name, alias(similarity, "jaccard_similarity"))
print(response.results)
# Output:
# name name2 jaccard_similarity
# 0 Alice Alice 1.00
# 1 Alice Bob 0.20
# 2 Alice Carol 0.25
# 3 Bob Alice 0.20
# 4 Bob Bob 1.00
# 5 Bob Carol 0.25
# 6 Carol Alice 0.25
# 7 Carol Bob 0.25
# 8 Carol Carol 1.00
Compute.adamic_adar(node1: Producer, node2: Producer) -> Expression

Compute the Adamic-Adar index between two nodes in a graph. The Adamic-Adar index quantifies node similarity based on shared neighbors. Values are non-negative. The algorithm accepts directed graphs as input but processes them as undirected by ignoring edge directionality. In link prediction analysis, a high Adamic-Adar value may indicate that two nodes are likely to form a connection if they do not already have one. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesEdge direction is ignored; the graph is treated as undirected.
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object that produces the Adamic-Adar index between the two nodes as a floating-point value.

The Adamic-Adar index is a link prediction algorithm that estimates the likelihood of a connection forming between two nodes based on their shared neighbors. It assigns higher scores to node pairs that share rare or exclusive neighbors—under the idea that such neighbors are more informative than popular ones. The index is always non-negative, and higher values suggest a greater similarity between nodes. Although it accepts directed graphs, the algorithm treats the graph as undirected by ignoring edge directionality. In practice, it is often used to predict potential new links in a network or to rank node similarity.

This implementation follows the classic Adamic-Adar formulation:

where denotes the neighborhood of node . The sum is taken over the shared neighbors of and , and each term is weighted inversely by the logarithm of that neighbor’s degree. This weighting penalizes popular nodes (which appear in many neighborhoods) and emphasizes uncommon neighbors, which tend to carry more predictive power. By design, this metric is symmetric and purely topological, relying only on graph structure rather than node or edge attributes. Because it ignores directionality, users should preprocess or interpret results accordingly in directed networks. This implementation does not incorporate edge weights; all shared neighbors are treated equally regardless of any weight metadata present in the graph.

Use .adamic_adar() to compute the Adamic-Adar index between two nodes in a graph. You access the .adamic_adar() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.friends.extend([bob, carol])
# Create an undirected graph with Person nodes and edges between friends.
# This graph has two edges: one between Alice and Bob, and one between Bob and Carol.
# Even when creating a directed graph by setting `undirected=False`,
# the output remains identical to the undirected case.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Compute the Adamic-Adar index between each pair of people in the graph.
with model.query() as select:
person1, person2 = Person(), Person()
similarity = graph.compute.adamic_adar(person1, person2)
response = select(person1.name, person2.name, alias(similarity, "adamic_adar"))
print(response.results)
# Output:
# name name2 adamic_adar
# 0 Alice Alice inf
# 1 Bob Bob 1.442695
# 2 Bob Carol 1.442695
# 3 Carol Bob 1.442695
# 4 Carol Carol 1.442695

There is no row for Alice and Bob in the preceding query’s results. That’s because Alice and Bob have a an Adamic-Adar index of 0.0. Pairs of nodes with zero index, indicating no meaningful similarity, are often excluded from analyses. Consequently, we filter out these pairs to improve performance.

If node1 or node2 is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Create the union of the Person and Company types.
PersonOrCompany = Person | Company
with model.query() as select:
# Get all person and company objects.
obj1, obj2 = PersonOrCompany(), PersonOrCompany()
obj1 < obj2 # Ensure pairs are unique. Compares internal object IDs.
# Compute the Adamic-Adar index between each pair of objects.
# Objects that are not nodes in the graph are filtered.
similarity = graph.compute.adamic_adar(obj1, obj2)
response = select(obj1.name, obj2.name, alias(similarity, "adamic_adar"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name name2 adamic_adar
# 0 Carol Bob 1.442695
Compute.common_neighbor(node1: Producer, node2: Producer) -> Expression

Find the common neighbors between two nodes in a graph. In an undirected graph, a node w is a common neighbor of nodes u and v if there are edges connecting w to both u and v. The algorithm accepts directed graphs as input but processes them as undirected by ignoring edge directionality. Namely, in a directed graph, a node w is a common neighbor of nodes u and v if there are edges between w and both u and v, regardless of the direction of those edges.

Must be called in a rule or query context.

Graph TypeSupportedNotes
UndirectedYes
DirectedYesEdge direction is ignored; the graph is treated as undirected.
WeightedYesResults do not depend on the edge weights.
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object that produces nodes that are common neighbors of node1 and node2.

The common neighbor algorithm identifies nodes that are connected to both input nodes and is used to explore relationships and connectivity patterns. This algorithm is particularly useful in link prediction tasks, where it can help identify potential connections between nodes based on shared neighbors.

For a graph with vertex set and edge set , the common neighbor algorithm computes the set of common neighbors of two nodes and as follows:

In a directed graph, the algorithm treats the graph as undirected by ignoring edge directionality. This means that a node is a common neighbor of nodes and if there are edges between and both and , regardless of the direction of those edges. The algorithm is symmetric, meaning that the order of node1 and node2 does not affect the result.

Use .common_neighbor() to find the common neighbors between two nodes in a graph. You access the .common_neighbor() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.friends.extend([bob, carol])
# Create an undirected graph with Person nodes and edges between friends.
# This graph has two edges: one between Alice and Bob, and one between Alice and Carol.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Find the common neighbors of Bob and Carol.
with model.query() as select:
neighbor = graph.compute.common_neighbor(Person(name="Bob"), Person(name="Carol"))
response = select(neighbor.name)
print(response.results)
# Output:
# name
# 0 Alice

If node1 or node2 is not a node in the graph, no exception is raised. Instead, that pair of objects is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Create the union of the Person and Company types.
PersonOrCompany = Person | Company
with model.query() as select:
# Get all pairs of person and company objects.
obj1, obj2 = PersonOrCompany(), PersonOrCompany()
obj1 < obj2 # Ensure pairs are unique.
neighbor = graph.compute.common_neighbor(obj1, obj2)
response = select(obj1.name, obj2.name, neighbor.name)
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name name2 name3
# 0 Carol Bob Alice

Note that pairs of nodes with no common neighbors are filtered by .common_neighbor(). For example, there is no row for Alice and Bob in the above output because they have no common neighbors. Alice is neighbors with Bob and Carol, but Bob’s only neighbor is Alice.

.preferential_attachment() Preview

Section titled “.preferential_attachment() ”
Compute.preferential_attachment(node1: Producer, node2: Producer) -> Expression

Compute the preferential attachment score between two nodes in a graph. The preferential attachment score quantifies node similarity based on the product of their number of neighbors. In link prediction analysis, a high preferential attachment score may indicate that two nodes are likely to form a connection under the assumption that connections are more likely between nodes with higher degrees. Must be called in a rule or query context.

Graph TypeSupportedNotes
UndirectedYes
DirectedYesOperates on the undirected version of the graph.
WeightedYesResults do not depend on the edge weights.
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object.

Preferential attachment estimates the likelihood of two nodes forming a connection based on their degree. It is grounded in the idea that nodes with more connections are more likely to attract new links—a phenomenon often described as “the rich get richer.” The score is calculated as the product of the number of neighbors (the degree) of the two input nodes. In many real-world networks, such as citation networks and social graphs, this principle helps explain the emergence of hub nodes and power-law degree distributions. Formally, given nodes and , the preferential attachment score is:

where and are the number of neighbors of nodes and , respectively.

Note that this score is zero whenever either or is an isolated node. Our implementation supports directed and undirected graphs, but directionality is ignored: the algorithm operates on the undirected version of the input graph. It also supports weighted and unweighted graphs, but weights are ignored, and the score is based purely on node degrees.

Use .preferential_attachment() to compute the preferential attachment score between two nodes in a graph. You access the .preferential_attachment() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.friends.add(bob)
# Create an undirected graph with Person nodes and edges between friends.
# This graph has one edge between Alice and Bob. Carol is not connected to anyone.
# Even when creating a directed graph by setting `undirected=False`,
# the output remains identical to the undirected case.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Compute the preferential attachment score between Alice and Bob.
with model.query() as select:
person1, person2 = Person(), Person()
similarity = graph.compute.preferential_attachment(person1, person2)
response = select(person1.name, person2.name, alias(similarity, "preferential_attachment"))
print(response.results)
# Output:
# name name2 preferential_attachment
# 0 Alice Alice 1
# 1 Alice Bob 1
# 2 Alice Carol 0
# 3 Bob Alice 1
# 4 Bob Bob 1
# 5 Bob Carol 0
# 6 Carol Alice 0
# 7 Carol Bob 0
# 8 Carol Carol 0

While many graph algorithms produce sparse results (node pairs with a zero score are omitted), the preferential_attachment algorithm returns dense results, meaning node pairs with a zero score are still returned.

For example, consider Alice and Carol. Even though Carol has no neighbors, the preferential_attachment algorithm still calculates a score (in this case, 0) for the pair (Alice, Carol).

If node1 or node2 is not a node in the graph, no exception is raised. Instead, that object is filtered from the rule or query:

# Add a Company type to the model.
Company = model.Type("Company")
# Add some companies to the model.
with model.rule():
apple = Company.add(name="Apple")
google = Company.add(name="Google")
# Create the union of the Person and Company types.
PersonOrCompany = Person | Company
with model.query() as select:
# Get all person and company objects.
obj1, obj2 = PersonOrCompany(), PersonOrCompany()
obj1 < obj2 # Ensure pairs are unique. Compares internal object IDs.
# Compute the preferential attachment score between each pair of objects.
# Objects that are not nodes in the graph are filtered.
similarity = graph.compute.preferential_attachment(obj1, obj2)
response = select(obj1.name, obj2.name, alias(similarity, "preferential_attachment"))
# Only rows for people are returned, since companies are not nodes in the graph.
print(response.results)
# Output:
# name name2 preferential_attachment
# 0 Alice Bob 1
# 1 Carol Alice 0
# 2 Carol Bob 0
Compute.infomap(
node: Producer
max_levels: int = 1,
max_sweeps: int = 20,
level_tolerance: float = 0.01,
sweep_tolerance: float = 0.0001,
teleportation_rate: float = 0.15,
visit_rate_tolerance: float = 1e-15,
randomization_seed: int | None = None,
) -> Expression

Assign a community label to node using the Infomap algorithm. Infomap leverages principles from information theory to detect communities within networks. It employs the map equation, a powerful tool that quantifies the information needed to describe random walks on a network. This method aims to identify the most efficient encoding of network structure, thereby optimizing for the most compact representation of community divisions. Nodes assigned to the same community are more likely to interact or communicate with one another as information flows through the network. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesOnly positive weights are supported.
UnweightedYes
NameTypeDescriptionRange
nodeProducerA node in the graph.Vertex set.
max_levelsintThe maximum number of levels at which to optimize. Default is 1.Positive integer.
max_sweepsintThe maximum number of iterations to run at each level. Default is 20.Non-negative integer.
level_tolerancefloatThe minimum change in the map equation required to continue to the next level. Default is 0.01.Small positive number or zero.
sweep_tolerancefloatThe minimum change in the map equation required to continue to the next sweep. Default is 0.0001.Small positive number or zero.
teleportation_ratefloatThe non-zero probability of teleporting to a random node. Default is 0.15.[1e-4, 1].
visit_rate_tolerancefloatThe minimum change in the visit rate required to continue to the next sweep. Default is 1e-15.Small positive number.
randomization_seedint or NoneThe seed for the algorithm’s random number generator. Default is None.None or non-negative integer.

Returns an Expression object that produces the integer community label assigned by Infomap to node as an integer value.

The Infomap algorithm is a community detection method that identifies modules in a graph by optimizing the representation of information flow. Instead of relying on structural properties like edge density, Infomap treats the graph as a dynamic system where a random walker moves between nodes, and it seeks to compress the description of this walk by grouping nodes into modules that retain most of the flow. This makes Infomap particularly effective for uncovering community structure in networks with directional or weighted edges, such as citation networks, biological pathways, and social graphs. It is widely regarded for its high accuracy and ability to detect overlapping and hierarchical communities.

Infomap operates by modeling the network as a random walk process, where a walker moves from node to node based on edge weights or probabilities. The algorithm then seeks to minimize the expected code length of the walker’s trajectory by grouping nodes into communities (modules) that are densely connected internally but sparsely connected to other modules. This is achieved by assigning unique codewords to nodes within modules and module identifiers themselves, allowing for efficient encoding of the network structure. The algorithm’s core is the map equation, which quantifies the information needed to describe the random walk on the network. By minimizing this equation, Infomap identifies partitions that reveal the underlying community structure of the graph. The algorithm can operate at multiple levels, allowing it to uncover both coarse and fine-grained structures within the network.

Use .infomap() to assign community labels to nodes in a graph using the Infomap algorithm. You access the .infomap() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
evelyn = Person.add(name="Evelyn")
alice.follows.add(bob)
carol.follows.add(daniel)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Find the community label for a single person using the Infomap algorithm.
with model.query() as select:
community = graph.compute.infomap(Person(name="Alice"))
response = select(alias(community, "community_label"))
print(response.results)
# Output:
# community_label
# 0 2
# Find the community label for each person in the graph.
with model.query() as select:
person = Person()
community = graph.compute.infomap(person)
response = select(person.name, alias(community, "community_label"))
print(response.results)
# name community_label
# 0 Alice 2
# 1 Bob 2
# 2 Carol 1
# 3 Daniel 1
# 4 Evelyn 3

In this example, .infomap() finds three communities in the graph: Alice and Bob are in one community, and Carol and Daniel are in another. Evelyn is an isolated node and has been assigned a unique community ID.

Compute.label_propagation(
node: Producer,
max_sweeps: int = 20,
randomization_seed: int | None = None
) -> Expression

Assign a community label to node using the label propagation algorithm. Label propagation is an algorithm that begins by assigning each node a unique community label and iteratively updates the label of each node to the most common label among its neighbors until convergence or a maximum number of iterations is reached. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesOnly positive weights are supported.
UnweightedYes
NameTypeDescriptionRange
nodeProducerA node in the graph.Vertex set.
max_sweepsintThe maximum number of iterations to run the label propagation algorithm. Default is 20.Non-negative integer.
randomization_seedint or NoneThe seed for the algorithm’s random number generator. Default is None.None or non-negative integer.

Label Propagation is a community detection algorithm that identifies clusters of densely connected nodes in a graph. It works by assigning an initial label to each node and iteratively updating each node’s label based on its neighbors. Over time, groups of connected nodes converge to the same label, revealing underlying community structures. This method is simple, scalable, and works well on large graphs where the goal is to find natural clusters without predefining the number of communities.

This implementation uses a variant of asynchronous label propagation, designed to increase convergence speed and reliability. At each step, a node updates its label by examining its in-neighbors—nodes with edges pointing to it—and summing the weights of incoming edges for each label. The label with the highest cumulative weight is chosen; in the case of a tie, the algorithm prefers to retain the node’s current label but otherwise selects randomly among tied labels. These design choices, including strategic tie-breaking and weight-based label selection, improve stability across runs and increase the chances of convergence. Our implementation does not currently support seeding of community labels.

Returns an Expression object that produces the integer community label assigned by label propagation to node.

Use .label_propagation() to assign a community label to a node in a graph. You access the .label_propagation() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
evelyn = Person.add(name="Evelyn")
alice.follows.add(bob)
carol.follows.add(daniel)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Find the community label for a single person using label propagation.
with model.query() as select:
person = Person(name="Alice")
community = graph.compute.label_propagation(person)
response = select(alias(community, "community_label"))
print(response.results)
# Output:
# community_label
# 0 2
# Find the community label for each person in the graph.
with model.query() as select:
person = Person()
community = graph.compute.label_propagation(person)
response = select(person.name, alias(community, "community_label"))
print(response.results)
# Output:
# name community_label
# 0 Alice 2
# 1 Bob 2
# 2 Carol 1
# 3 Daniel 1
# 4 Evelyn 3

In this example, .label_propagation() identifies three communities: (1) Alice and Bob, (2) Carol and Daniel, and (3) Evelyn as an isolated community.

Compute.louvain(
node: Producer,
max_levels: int = 1,
max_sweeps: int = 20,
level_tolerance: float = 0.01,
sweep_tolerance: float = 0.0001,
randomization_seed: int | None = None
) -> Expression

Assign a community label to node using the Louvain method. The Louvain algorithm is a hierarchical community detection technique that initially assigns each node to its own community. It iteratively merges nodes and communities to maximize modularity, a metric that evaluates the density of edges within communities relative to edges between them. The process continues until no further improvements in modularity can be made, or the maximum number of iterations is reached. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedNoCurrently not supported.
UndirectedYes
WeightedYesOnly positive weights are supported.
UnweightedYes
NameTypeDescriptionRange
nodeProducerA node in the graph.Vertex set.
max_levelsintThe maximum number of levels at which to optimize. Default is 1.Positive integer.
max_sweepsintThe maximum number of iterations to run at each level. Default is 20.Non-negative integer.
level_tolerancefloatThe minimum change in modularity required to continue to the next level. Default is 0.01.Small positive number or zero.
sweep_tolerancefloatThe minimum change in modularity required to continue to the next sweep. Default is 0.0001.Small positive number or zero.
randomization_seedint or NoneThe seed for the algorithm’s random number generator. Default is None.None or non-negative integer.

Returns an Expression object that produces the integer community label assigned to node by the Louvain algorithm.

The Louvain algorithm is a popular and efficient method for uncovering community structure in large undirected graphs. It assigns each node a community label by maximizing modularity, a measure that compares the density of edges within communities to the density between them. The algorithm proceeds hierarchically: it starts by assigning each node to its own community and iteratively merges communities to improve modularity. This process continues across multiple levels until no further improvement is possible, or a user-defined limit on levels or iterations is reached. Louvain is widely used in network science, social analysis, and biological networks due to its scalability and ability to reveal hierarchical community structure.

This implementation supports undirected, weighted, and unweighted graphs (weights must be positive). It uses an iterative two-phase process: first, it locally optimizes modularity by reassigning nodes to neighboring communities, and second, it aggregates the graph by collapsing communities into super-nodes and repeating the process. The algorithm terminates early if modularity gain falls below user-specified tolerances: sweep_tolerance controls inner-loop convergence, level_tolerance controls when to stop moving to higher levels of aggregation, randomization_seed (optional) ensures reproducibility when random decisions are made. max_levels and max_sweeps allow fine control over performance versus accuracy.

Use .louvain() to assign community labels to nodes in a graph using the Louvain algorithm. You access the .louvain() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friends` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
evelyn = Person.add(name="Evelyn")
alice.friends.add(bob)
carol.friends.add(daniel)
# Create an undirected graph with Person nodes and edges between friends.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Find the community label for a single person using Louvain.
with model.query() as select:
community = graph.compute.louvain(Person(name="Alice"))
response = select(alias(community, "community_label"))
print(response.results)
# Output:
# community_label
# 0 2
# Find the community label for each person using Louvain.
with model.query() as select:
person = Person()
community = graph.compute.louvain(person)
response = select(person.name, alias(community, "community_label"))
print(response.results)
# name community_label
# 0 Alice 2
# 1 Bob 2
# 2 Carol 1
# 3 Daniel 1
# 4 Evelyn 3

In this example, .louvain() finds three communities in the graph: Alice and Bob are in one community, and Carol and Daniel are in another. Evelyn is an isolated node and has been assigned a unique community ID.

Compute.triangle_community(node: Producer) -> Expression

Assign a community label to node using the percolation method. The percolation method identifies communities within a graph by detecting densely connected clusters of nodes. It operates by initially locating all triangles within the graph—groups of three interconnected nodes. The process involves iteratively merging these triangles that share a common edge. This merging continues until no further triangles can be combined, effectively revealing the network’s densely connected communities. Nodes that are not part of any triangles are excluded. This method is only applicable to undirected graphs. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedNoNot applicable.
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces the community label of node as an integer value.

The triangle community algorithm detects communities in a graph by identifying and expanding clusters of densely connected nodes. It begins by locating all triangles—sets of three mutually connected nodes—and then merges triangles that share at least one edge. This iterative merging process continues until no more triangles can be combined, resulting in communities defined by overlapping triangles. Nodes not participating in any triangle are excluded from the output. This method is particularly effective at identifying tight-knit groups in undirected graphs and is useful in applications like social network analysis, fraud detection, and biological interaction networks.

Internally, the algorithm constructs a graph where each node represents a triangle in the original graph, and edges connect triangles that share a common edge (equivalently share exactly two vertices). It then computes the connected components of this triangle graph to identify percolated communities. The final output maps each original graph node (that appears in at least one triangle) to a unique community label. The method requires an undirected graph and ignores edge weights, treating all connections as unweighted. Since the algorithm only considers triangles, it is most effective in graphs where triadic closure is common.

Use .triangle_community() to assign a community label to a node in a graph. You access the .triangle_community() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friends` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
charlie = Person.add(name="Charlie")
diana = Person.add(name="Diana")
alice.friends.add(bob)
bob.friends.add(charlie)
charlie.friends.extend([alice, diana])
# Create an undirected graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Find the community label for a single person using the percolation method.
with model.query() as select:
community = graph.compute.triangle_community(Person(name="Alice"))
response = select(alias(community, "community_label"))
print(response.results)
# Output:
# community_label
# 0 1
# Compute the community label for each person in the graph.
with model.query() as select:
person = Person()
community = graph.compute.triangle_community(person)
response = select(person.name, alias(community, "community_label"))
print(response.results)
# Output:
# name community_label
# 0 Alice 1
# 1 Bob 1
# 2 Charlie 1

The above graph has edges from Alice to Bob, Bob to Charlie, Charlie to Alice, and Charlie to Diana. Alice, Bob, and Charlie form a triangle, but Diana is not part of any triangles. The percolation method assigns Alice and Bob to the same community. Diana is filtered out because she is not part of any triangles.

Use std.aggregate.count() to count the number of communities identified in the graph:

from relationalai.std.aggregates import count
with model.query() as select:
person = Person()
community = graph.compute.triangle_community(person)
response = select(alias(count(community), "num_communities"))
print(response.results)
# Output:
# num_communities
# 0 1

.avg_clustering_coefficient() Preview

Section titled “.avg_clustering_coefficient() ”
Compute.avg_clustering_coefficient() -> Expression

Compute the average clustering coefficient of all nodes in an undirected graph. The average clustering coefficient is the average of the local clustering coefficients for each node in the graph. Values range from 0 to 1. Higher average clustering coefficients indicate nodes’ increased tendency to form triangles with neighbors. Standard definitions of clustering coefficients primarily apply to undirected graphs. Directed graphs are not supported as a valid input.

Must be called in a rule or query context.

  • Directed graphs are not supported.
Graph TypeSupportedNotes
DirectedNo
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes

Returns an Expression object that produces the average clustering coefficient of all nodes in the graph as a floating-point value.

Use .avg_clustering_coefficient() to compute the average clustering coefficient of all nodes in an undirected graph. You access the .avg_clustering_coefficient() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.friends.extend([bob, carol])
bob.friends.extend([alice, carol, daniel])
# Create an undirected graph with Person nodes and edges between friends.
# This graph has four edges: Alice, Bob, and Carol form a triangle, and Daniel is only connected to Bob.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Compute the average clustering coefficient of the graph.
with model.query() as select:
acc = graph.compute.avg_clustering_coefficient()
response = select(alias(acc, "avg_clustering_coefficient"))
print(response.results)
# Output:
# avg_clustering_coefficient
# 0 0.583333
Compute.is_triangle(node1: Producer, node2: Producer, node3: Producer) -> Expression

Filters node1, node2, and node3 for combinations of that form a triangle. In an undirected graph, a triangle is a set of three nodes where each node is connected to the other two nodes. In a directed graph, a triangle is a set of three nodes where there is an edge from the first node to the second node, an edge from the second node to the third node, and an edge from the third node to the first node. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.
node3ProducerA node in the graph.

An Expression object.

Use .is_triangle() to check if three nodes form a triangle in a graph. You access the .is_triangle() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
charlie = Person.add(name="Charlie")
diana = Person.add(name="Diana")
alice.follows.add(bob)
bob.follows.add(charlie)
charlie.follows.extend([alice, diana])
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by Default.
# This graph has edges from Alice to Bob, Bob to Charlie, Charlie to Alice, and Charlie to Diana.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Find all nodes that form a triangle
with model.query() as select:
# Get triples of Person objects.
person1, person2, person3 = Person(), Person(), Person()
# Filter triples based on whether they form a triangle.
graph.compute.is_triangle(person1, person2, person3)
# Select the names of people that form triangles.
response = select(person1.name, person2.name, person3.name)
print(response.results)
# Output:
# name name2 name3
# 0 Alice Bob Charlie
# 1 Bob Charlie Alice
# 2 Charlie Alice Bob

The output has three rows, but each row is a permutation of the same three nodes.

Compute.local_clustering_coefficient(node: Producer) -> Expression

Compute the local clustering coefficient of a node in an undirected graph. The local clustering coefficient of a node represents the proportion of pairs among the node’s neighbors that are also connected to each other. Values range from 0 to 1, where 0 indicates none of the node’s neighbors are connected, and 1 indicates that the node’s neighbors are fully connected, forming a clique. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedNoNot applicable.
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object.

The local clustering coefficient measures how close a node’s neighborhood is to forming a complete subgraph (clique). Specifically, it quantifies the fraction of a node’s neighbor pairs that are also connected to each other. The value ranges from 0 (none of the neighbors are connected) to 1 (all neighbors are mutually connected). This metric provides insight into the local cohesion of a graph, revealing nodes that act as hubs within tightly knit communities. It applies to undirected graphs only and is commonly used in social network analysis, biological networks, and graph-based anomaly detection.

Formally, for a node v with degree ​, the local clustering coefficient is defined as:

where is the number of edges between the neighbors of . The denominator counts the total number of possible edges between neighbors, and the factor of 2 adjusts for double-counting in undirected graphs. Nodes with degree less than 2 are assigned a clustering coefficient of 0, as no triangles are possible. This implementation efficiently scans the adjacency structure of each node to identify neighbor connections and must be invoked in a rule or query context.

Use .local_clustering_coefficient() to compute the local clustering coefficient of a node in an undirected graph. You access the .local_clustering_coefficient() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `friend` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.friends.extend([bob, carol])
bob.friends.extend([alice, carol, daniel])
# Create an undirected graph with Person nodes and edges between friends.
# This graph has four edges: Alice, Bob, and Carol form a triangle, and Daniel is only connected to Bob.
graph = Graph(model, undirected=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.friends)
# Compute the local clustering coefficient of each person.
with model.query() as select:
person = Person()
lcc = graph.compute.local_clustering_coefficient(person)
response = select(person.name, alias(lcc, "clustering_coefficient"))
print(response.results)
# Output:
# name clustering_coefficient
# 0 Alice 1.000000
# 1 Bob 0.333333
# 2 Carol 1.000000
# 3 Daniel 0.000000
# Compute the local clustering coefficient of a specific person.
with model.query() as select:
lcc = graph.compute.local_clustering_coefficient(Person(name="Alice"))
response = select(alias(lcc, "clustering_coefficient"))
print(response.results)
# Output:
# clustering_coefficient
# 0 1.0
Compute.num_triangles(node: Producer | None = None) -> Expression

Compute the number of unique triangles in the graph. A triangle is a set of three nodes x, y, and z such that there is an edge between x and y, y and z, and z and x. If node is not None, the number of unique triangles that node is part of is computed. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducer or NoneA node in the graph. If not None, the number of unique triangles that node is part of is computed. Otherwise, the total number of unique triangles in the graph is computed. Default is None.

Returns an Expression object that produces the number of unique triangles in the graph as an integer value, if node is None, or the number of unique triangles that node is part of as an integer value, if node is not None.

Use .num_triangles() to compute the number of unique triangles in a graph. You access the .num_triangles() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
charlie = Person.add(name="Charlie")
diana = Person.add(name="Diana")
alice.follows.add(bob)
bob.follows.add(charlie)
charlie.follows.extend([alice, diana])
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Compute the number of unique triangles in the graph.
with model.query() as select:
num_triangles = graph.compute.num_triangles()
response = select(alias(num_triangles, "num_triangles"))
print(response.results)
# Output:
# num_triangles
# 0 1
# Compute the number of unique triangles that each node is part of.
with model.query() as select:
person = Person()
num_triangles = graph.compute.num_triangles(person)
response = select(person.name, alias(num_triangles, "num_triangles"))
print(response.results)
# Output:
# name num_triangles
# 0 Alice 1
# 1 Bob 1
# 2 Charlie 1
# 3 Diana 0
Compute.triangles(node: Producer | None = None) -> tuple[Expression, Expression, Expression]

Find all unique triangles in the graph. A triangle is a set of three nodes x, y, and z such that there is an edge between x and y, y and z, and z and x. If node is not None, the unique triangles that node is part of are computed. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducer or NoneA node in the graph. If not None, the unique triangles that node is part of are computed. Otherwise, the total number of unique triangles in the graph is computed. Default is None.

Returns a tuple (node1, node2, node3) of three Expression objects that produce triples of nodes that form unique triangles in the graph.

For undirected graphs, triples are produced so that the nodes are ordered in ascending order by their internal identifiers. In directed graphs, .triangles() produces triples of nodes such that node1 < node2, node1 < node3, and node2 != node3. This ensures that each triangle is unique based on the ordering of nodes and edge directions. For instance, (1, 2, 3) and (1, 3, 2) denote distinct directed triangles.

Use .triangles() to find all unique triangles in a graph. You access the .triangles() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
charlie = Person.add(name="Charlie")
diana = Person.add(name="Diana")
alice.follows.add(bob)
bob.follows.add(charlie)
charlie.follows.extend([alice, diana])
diana.follows.add(bob)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
graph = Graph(model)
graph.Edge.extend(Person.follows)
# Compute the unique triangles in the graph.
with model.query() as select:
person1, person2, person3 = graph.compute.triangles()
response = select(person1.name, person2.name, person3.name)
print(response.results)
# Output:
# name name2 name3
# 0 Charlie Alice Bob
# 1 Charlie Diana Bob
# Compute the unique triangles that include Alice.
with model.query() as select:
alice = Person(name="Alice")
person1, person2, person3 = graph.compute.triangles(alice)
response = select(person1.name, person2.name, person3.name)
print(response.results)
# Output:
# name name2 name3
# 0 Charlie Alice Bob
Compute.is_connected() -> Expression

Check if the graph is connected. A graph is connected if every node is reachable from every other node in the undirected version of the graph. For directed graphs, connected is the same as weakly connected. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes

Returns an Expression object that filters connected graphs.

A graph is considered connected if there is a path between every pair of nodes when treated as an undirected graph. For directed graphs, this means that every node can be reached from every other node when ignoring edge directions (weakly connected).

Use .is_connected() to check if a graph is connected. You access the .is_connected() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork1000")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.add(bob)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has one edge from Alice to Bob. Carol is not connected to anyone.
graph = Graph(model, with_isolated_nodes=True)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Is the graph connected?
with model.query() as select:
with model.match() as connected:
with graph.compute.is_connected():
connected.add(True)
with model.case():
connected.add(False)
response = select(alias(connected, "is_connected"))
print(response.results)
# Output:
# is_connected
# 0 False
Compute.is_reachable(node1: Producer, node2: Producer) -> Expression

Check if node2 is reachable from node1 in the graph. One node is reachable from another if there is a path from the first node to the second. Every node is reachable from itself. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on the weights.
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression that filters pairs of nodes in which there is a path from node1 to node2.

Use .is_reachable() to check if one node is reachable from another in a graph. You access the .is_reachable() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.add(bob)
bob.follows.add(carol)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has edges from Alice to Bob and Bob to Carol.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Can Alice reach Carol?
with model.query() as select:
alice = Person(name="Alice")
carol = Person(name="Carol")
with model.match() as reachable:
with model.case():
graph.compute.is_reachable(alice, carol)
reachable.add(True)
with model.case():
reachable.add(False)
response = select(alias(reachable, "is_reachable"))
print(response.results)
# Output:
# is_reachable
# 0 True
# Who can reach Alice?
with model.query() as select:
person = Person()
graph.compute.is_reachable(person, Person(name="Alice"))
response = select(person.name)
# The only person that can reach Alice is herself.
print(response.results)
# Output:
# name
# 0 Alice

To find all nodes reachable from a given node, use .reachable_from(). Use .distance() to find the shortest path length between two nodes.

Compute.reachable_from(node: Producer) -> Expression

Find all nodes reachable from node in a graph. One node is reachable from another if there is a path from the first node to the second. Every node is reachable from itself. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYes
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

Returns an Expression object that produces model objects that are reachable from node.

Use .reachable_from() to find all nodes reachable from a node in a graph. You access the .reachable_from() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.add(bob)
bob.follows.add(carol)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has edges from Alice to Bob and Bob to Carol.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Who is reachable from Alice?
with model.query() as select:
reachable = graph.compute.reachable_from(Person(name="Alice"))
response = select(reachable.name)
print(response.results)
# Output:
# name
# 0 Alice
# 1 Bob
# 2 Carol

In the example above, both Bob and Carol are reachable from Alice because there is a path from Alice to Bob and a path from Alice to Carol that passes through Bob. Every node is reachable from itself, so Alice is also reachable from Alice.

To check if one node is reachable from another, use .is_reachable(). Use .distance() to find the shortest path length between two nodes.

Compute.weakly_connected_component(node: Producer) -> Expression

Find the weakly connected component containing node in a graph. The weakly connected component of a node is the set of nodes that are reachable from the node in an undirected version of the graph. Components are identified by the node in the component with the smallest internal identifier. In an undirected graph, the weakly connected component is the same as the connected component. Must be called in a rule or query context.

Graph TypeSupportedNotes
DirectedYesOperates on the undirected version of the graph.
UndirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes
NameTypeDescription
nodeProducerA node in the graph.

An Expression object that produces the representative node of the weakly connected component containing node. Component representatives are the nodes with the smallest internal identifiers in the component.

Use .weakly_connected_component() to find the weakly connected component containing a node in a graph. You access the .weakly_connected_component() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.add(bob)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has one edge from Alice to Bob. Carol is not connected to anyone.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Compute the weakly connected component for each person in the graph.
with model.query() as select:
person = Person()
component = graph.compute.weakly_connected_component(person)
response = select(person.name, alias(component.name, "component_representative"))
print(response.results)
# Output:
# name component_representative
# 0 Alice Alice
# 1 Bob Alice
# 2 Carol Carol

Component representatives are the objects with the smallest internal identifiers in the component. Above, Alice and Bob are in the same component, with Alice as the representative. Carol is in a component by herself because she is not connected to anyone.

Use std.aggregates.count to count the number of weakly connected components in a graph:

from relationalai.std.aggregates import count
with model.query() as select:
person = Person()
component = graph.compute.weakly_connected_component(person)
response = select(alias(count(component), "num_components"))
print(response.results)
# Output:
# num_components
# 0 2
Compute.diameter_range() -> tuple[Expression, Expression]

Compute lower and upper bounds for the maximum shortest-path distance between any two path-connected pairs of nodes. Edge weights are ignored in weighted graphs. If the graph is disconnected, the upper and lower bounds of the connected component with the most nodes is returned. Must be called in a rule or query context.

Graph TypeSupportedNotes
UndirectedYes
DirectedYes
WeightedYesResults do not depend on edge weights.
UnweightedYes

Returns a tuple of two Expression objects that produce the lower and upper bounds for the diameter of the graph. In the case of directed graphs, these bounds apply only to pairs of nodes where a path exists from one to the other.

The diameter range is computed by selecting a subset of highest degree nodes and, for each node, finding the length of the longest shortest path from that node to the rest of the graph. The minimum and maximum of these lengths are returned as the lower and upper bounds of the diameter, respectively.

Use .diameter_range() to compute the range of possible diameters in a graph. You access the .diameter_range() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
alice.follows.add(bob)
bob.follows.add(carol)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has edges from Alice to Bob and Bob to Carol.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Compute the diameter range of the graph.
with model.query() as select:
diam_min, diam_max = graph.compute.diameter_range()
response = select(alias(diam_min, "min"), alias(diam_max, "max"))
print(response.results)
# Output:
# min max
# 0 2 2

In cases like this where the lower and upper bounds are the same, the diameter of the graph is known exactly. This may not always be the case, especially for larger and more complex graphs.

Compute.distance(node1: Producer, node2: Producer) -> Expression

This algorithm computes the shortest path length between two nodes in a graph. Must be called in a rule or query context.

Graph TypeSupportedNotes
UndirectedYes
DirectedYes
WeightedYes
UnweightedYes
NameTypeDescription
node1ProducerA node in the graph.
node2ProducerA node in the graph.

Returns an Expression object that produces the shortest path length between node1 and node2 as an integer value.

Use .distance() to compute the shortest path length between two nodes in a graph. You access the .distance() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with a Person type.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
# Add some people to the model and connect them with a multi-valued `follows` property.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
alice.follows.add(bob)
bob.follows.add(carol)
carol.follows.add(daniel)
# Create a directed graph with Person nodes and edges between followers.
# Note that graphs are directed by default.
# This graph has three edges: one from Alice to Bob, Bob to Carol, and Carol to Daniel.
graph = Graph(model)
graph.Node.extend(Person)
graph.Edge.extend(Person.follows)
# Find the distance between Alice and Daniel.
with model.query() as select:
dist = graph.compute.distance(Person(name="Alice"), Person(name="Daniel"))
response = select(alias(dist, "distance"))
print(response.results)
# Output:
# distance
# 0 3
# Find all nodes at distance at most two from Alice.
with model.query() as select:
node = Person()
dist = graph.compute.distance(Person(name="Alice"), node)
dist <= 2
response = select(node.name, alias(dist, "distance"))
print(response.results)
# Output:
# name distance
# 0 Alice 0
# 1 Bob 1
# 2 Carol 2

Note that the distance between a node and itself is 0.

Use .distance() to compute the shortest path length between two nodes in a weighted graph. You access the .distance() method from a Graph object’s .compute attribute:

import relationalai as rai
from relationalai.std import alias
from relationalai.std.graphs import Graph
# Create a model named "socialNetwork" with Person and Friendship types.
model = rai.Model("socialNetwork")
Person = model.Type("Person")
Friendship = model.Type("Friendship")
# Add some people to the model and connect them with the Friendship type.
with model.rule():
alice = Person.add(name="Alice")
bob = Person.add(name="Bob")
carol = Person.add(name="Carol")
daniel = Person.add(name="Daniel")
Friendship.add(person1=alice, person2=bob, strength=1.0)
Friendship.add(person1=bob, person2=carol, strength=0.5)
Friendship.add(person1=carol, person2=daniel, strength=0.75)
# Create a directed graph with Person nodes and edges between friends.
# Note that graphs are directed by default.
# This graph has three edges: one from Alice to Bob, Bob to Carol, and Carol to Daniel.
# The edges are weighted by the strength of each friendship.
graph = Graph(model, weighted=True)
graph.Node.extend(Person)
with model.rule():
friendship = Friendship()
graph.Edge.add(friendship.person1, friendship.person2, weight=friendship.strength)
# Find the weighted distance between Alice and Daniel.
with model.query() as select:
dist = graph.compute.distance(Person(name="Alice"), Person(name="Daniel"))
response = select(alias(dist, "distance"))
print(response.results)
# Output:
# distance
# 0 2.25
# Find all nodes with weighted distance at most two from Alice.
with model.query() as select:
node = Person()
dist = graph.compute.distance(Person(name="Alice"), node)
dist <= 2.0
response = select(node.name, alias(dist, "distance"))
print(response.results)
# Output:
# name distance
# 0 Alice 0.0
# 1 Bob 1.0
# 2 Carol 1.5