Skip to content

Graph

relationalai.semantics.reasoners.graph.core
Graph(
model,
*,
directed: bool,
weighted: bool,
aggregator: Optional[str] = None,
node_concept: Optional[Concept] = None,
edge_concept: Optional[Concept] = None,
edge_src_relationship: Optional[Relationship | Chain] = None,
edge_dst_relationship: Optional[Relationship | Chain] = None,
edge_weight_relationship: Optional[Relationship | Chain] = None
)

A graph object.

  • model

    (Model) - The model to use for the graph.
  • directed

    (bool) - Whether the graph is directed.
  • weighted

    (bool) - Whether the graph is weighted.
  • aggregator

    (str | None, default: None) - The aggregation function to use for multi-edges.
  • node_concept

    (Concept | None, default: None) - The concept to use for the nodes in the graph.
  • edge_concept

    (Concept | None, default: None) - The concept to use for the edges in the graph.
  • edge_src_relationship

    (Relationship | Chain | None, default: None) - The relationship to use for the source nodes in the graph.
  • edge_dst_relationship

    (Relationship | Chain | None, default: None) - The relationship to use for the destination nodes in the graph.
  • edge_weight_relationship

    (Relationship | Chain | None, default: None) - The relationship to use for the edge weights in the graph.
Graph.directed: bool

Whether the graph is directed.

Graph.weighted: bool

Whether the graph is weighted.

Graph.Node: Concept

The nodes of the graph.

Graph.Edge: Concept

The edges of the graph.

Graph.EdgeSrc: Relationship | Chain

The relationship that determines source nodes for edges in the graph.

Possible Members:

Graph.EdgeDst: Relationship | Chain

The relationship that determines destination nodes for edges in the graph.

Possible Members:

Graph.EdgeWeight: Relationship | Chain

The relationship that determines edge weights in the graph.

Possible Members:

Graph.num_nodes() -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a unary relationship containing the number of nodes in the graph.

Returns:

  • Relationship - A unary relationship containing the number of nodes in the graph.

Relationship Schema:

num_nodes(count)

  • count (Integer): The number of nodes in the graph.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up the graph and concepts
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define some nodes
>>> node1, node2, node3, node4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(node1, node2, node3, node4)
>>>
>>> # 3. Define the full set of edges
>>> model.define(
... Edge.new(src=node1, dst=node2),
... Edge.new(src=node2, dst=node3),
... Edge.new(src=node3, dst=node3),
... Edge.new(src=node2, dst=node4),
... )
>>>
>>> # 4. The relationship contains the number of nodes
>>> graph.num_nodes().inspect()
num_nodes
0 4
Graph.num_edges() -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a unary relationship containing the number of edges in the graph.

Returns:

  • Relationship - A unary relationship containing the number of edges in the graph.

Relationship Schema:

num_edges(count)

  • count (Integer): The number of edges in the graph.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up the graph and concepts
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define some nodes
>>> node1, node2, node3, node4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(node1, node2, node3, node4)
>>>
>>> # 3. Define the edges
>>> model.define(
... Edge.new(src=node1, dst=node2),
... Edge.new(src=node2, dst=node3),
... Edge.new(src=node3, dst=node3),
... Edge.new(src=node2, dst=node4),
... )
>>>
>>> # 4. The relationship contains the number of edges
>>> graph.num_edges().inspect()
num_edges
0 4

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.neighbor(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing all neighbor pairs in the graph.

For directed graphs, a node’s neighbors include both its in-neighbors and out-neighbors.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the neighbor computation: only neighbors of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and one of its neighbors.

Relationship Schema:

neighbor(node, neighbor_node)

  • node (Node): A node in the graph.
  • neighbor_node (Node): A neighbor of the node.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... )
>>>
>>> # 3. Select the IDs from the neighbor relationship and inspect
>>> u, v = Node.ref("u"), Node.ref("v")
>>> neighbor = graph.neighbor()
>>> model.select(u.id, v.id).where(neighbor(u, v)).inspect()
id id_2
0 1 2
1 2 1
2 2 3
3 2 4
4 3 2
5 3 3
6 4 2
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute neighbors of
>>> # Define a subset containing only nodes 2 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>>
>>> # Get neighbors only of nodes in the subset
>>> constrained_neighbor = graph.neighbor(of=subset)
>>> model.select(u.id, v.id).where(constrained_neighbor(u, v)).inspect()
id id_2
0 2 1
1 2 3
2 2 4
3 3 2
4 3 3

Notes:

The neighbor() method, called with no parameters, computes and caches the full neighbor relationship, providing efficient reuse across multiple calls to neighbor(). In contrast, neighbor(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the neighbor relation is needed across a program, neighbor() is typically more efficient; this is the typical case. Use neighbor(of=subset) only when small subsets of the neighbor relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.inneighbor(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship of all nodes and their in-neighbors.

An in-neighbor of a node u is any node v where an edge from v to u exists. For undirected graphs, this is identical to neighbor.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the inneighbor computation: only in-neighbors of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a destination node and one of its in-neighbors.

Relationship Schema:

inneighbor(node, inneighbor_node)

  • node (Node): The destination node.
  • inneighbor_node (Node): The in-neighbor of the node (i.e., the source of an incoming edge).

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... )
>>>
>>> # 3. Select the IDs from the in-neighbor relationship and inspect
>>> node, inneighbor_node = Node.ref("node"), Node.ref("inneighbor_node")
>>> inneighbor = graph.inneighbor()
>>> model.select(
... node.id,
... inneighbor_node.id
... ).where(
... inneighbor(node, inneighbor_node)
... ).inspect()
id id_2
0 2 1
1 3 2
2 3 3
3 4 2
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute in-neighbors of
>>> # Define a subset containing only nodes 2 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>>
>>> # Get in-neighbors only of nodes in the subset
>>> constrained_inneighbor = graph.inneighbor(of=subset)
>>> model.select(node.id, inneighbor_node.id).where(constrained_inneighbor(node, inneighbor_node)).inspect()
id id_2
0 2 1
1 3 2
2 3 3

Notes:

The inneighbor() method, called with no parameters, computes and caches the full inneighbor relationship, providing efficient reuse across multiple calls to inneighbor(). In contrast, inneighbor(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the inneighbor relation is needed across a program, inneighbor() is typically more efficient; this is the typical case. Use inneighbor(of=subset) only when small subsets of the inneighbor relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.outneighbor(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship of all nodes and their out-neighbors.

An out-neighbor of a node u is any node v where an edge from u to v exists. For undirected graphs, this is identical to neighbor.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the outneighbor computation: only out-neighbors of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a source node and one of its out-neighbors.

Relationship Schema:

outneighbor(node, outneighbor_node)

  • node (Node): The source node.
  • outneighbor_node (Node): The out-neighbor of the node (i.e., the destination of an outgoing edge).

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the IDs from the out-neighbor relationship and inspect
>>> node, outneighbor_node = Node.ref("node"), Node.ref("outneighbor_node")
>>> outneighbor = graph.outneighbor()
>>> model.select(
... node.id,
... outneighbor_node.id
... ).where(
... outneighbor(node, outneighbor_node)
... ).inspect()
id id_2
0 1 2
1 2 3
2 2 4
3 3 3
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute out-neighbors of
>>> # Define a subset containing only nodes 2 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>>
>>> # Get out-neighbors only of nodes in the subset
>>> constrained_outneighbor = graph.outneighbor(of=subset)
>>> model.select(node.id, outneighbor_node.id).where(constrained_outneighbor(node, outneighbor_node)).inspect()
id id_2
0 2 3
1 2 4
2 3 3

Notes:

The outneighbor() method, called with no parameters, computes and caches the full outneighbor relationship, providing efficient reuse across multiple calls to outneighbor(). In contrast, outneighbor(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the outneighbor relation is needed across a program, outneighbor() is typically more efficient; this is the typical case. Use outneighbor(of=subset) only when small subsets of the outneighbor relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.common_neighbor(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a ternary relationship of common neighbor triplets.

A node w is a common neighbor of a pair of nodes u and v if w is a neighbor of both u and v.

Parameters:

  • full

    (bool, default: None) - If True, computes common neighbors for all pairs of nodes in the graph. This computation can be expensive for large graphs, as the result can scale quadratically in the number of edges or cubically in the number of nodes. Mutually exclusive with other parameters. Default is None.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the common neighbor computation: only common neighbors of node pairs where the first node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. Can only be used together with the from_ parameter. When provided with from_, constrains the domain of the common neighbor computation: only common neighbors of node pairs where the first node is in from_ and the second node is in to are computed and returned. Default is None.
  • between

    (Relationship, default: None) - A binary relationship containing pairs of nodes. When provided, constrains the domain of the common neighbor computation: only common neighbors for the specific node pairs in this relationship are computed and returned. Mutually exclusive with other parameters. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and one of their common neighbors.

Raises:

  • TypeError - If from_, to, or between is not a Relationship.
  • ValueError - If full is provided with any other parameter. If between is provided with any other parameter. If from_ is provided with any parameter other than to. If to is provided without the from_ parameter. If none of full, from_, or between is provided. If full is not True or None. If from_, to, or between is not attached to the same model as the graph. If from_, to, or between does not contain the graph’s Node concept. If from_ or to is not a unary relationship. If between is not a binary relationship.

Relationship Schema:

common_neighbor(node_u, node_v, common_neighbor_node)

  • node_u (Node): The first node in the pair.
  • node_v (Node): The second node in the pair.
  • common_neighbor_node (Node): The common neighbor of the pair.

Examples:

>>> from relationalai.semantics import Model, define, select
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Select the IDs from the common_neighbor relationship and inspect
>>> u, v, w = Node.ref("u"), Node.ref("v"), Node.ref("w")
>>> common_neighbor = graph.common_neighbor(full=True)
>>> model.select(
... u.id, v.id, w.id
... ).where(
... common_neighbor(u, v, w)
... ).inspect()
id id_2 id_3
0 1 1 2
1 1 3 2
2 1 4 2
3 2 2 1
4 2 2 3
5 2 2 4
6 2 3 3
7 2 3 4
8 2 4 3
9 3 1 2
10 3 2 3
11 3 2 4
12 3 3 2
13 3 3 3
14 3 3 4
15 3 4 2
16 3 4 3
17 4 1 2
18 4 2 3
19 4 3 2
20 4 3 3
21 4 4 2
22 4 4 3
>>> # 4. Use 'from_' parameter to constrain the set of nodes to compute common neighbors for
>>> # Define a subset containing only node 1
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 1).define(subset(node))
>>>
>>> # Get common neighbors only for pairs where first node is in subset
>>> constrained_common_neighbor = graph.common_neighbor(from_=subset)
>>> model.select(u.id, v.id, w.id).where(constrained_common_neighbor(u, v, w)).inspect()
id id_2 id_3
0 1 1 2
1 1 3 2
2 1 4 2
>>> # 5. Use both 'from_' and 'to' parameters to constrain the first two positions
>>> subset_a = model.Relationship(f"{Node:node} is in subset_a")
>>> subset_b = model.Relationship(f"{Node:node} is in subset_b")
>>> model.where(node.id == 1).define(subset_a(node))
>>> model.where(node.id == 3).define(subset_b(node))
>>>
>>> # Get common neighbors only where the first node is in subset_a and the second node is in subset_b
>>> constrained_common_neighbor = graph.common_neighbor(from_=subset_a, to=subset_b)
>>> model.select(u.id, v.id, w.id).where(constrained_common_neighbor(u, v, w)).inspect()
id id_2 id_3
0 1 3 2
>>> # 6. Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 1, node_b.id == 3).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 2, node_b.id == 4).define(pairs(node_a, node_b))
>>>
>>> # Get common neighbors only for the specific pairs (1, 3) and (2, 4)
>>> constrained_common_neighbor = graph.common_neighbor(between=pairs)
>>> model.select(u.id, v.id, w.id).where(constrained_common_neighbor(u, v, w)).inspect()
id id_2 id_3
0 1 3 2
1 2 4 3

Notes:

The common_neighbor(full=True) method computes and caches the full common neighbor relationship for all pairs of nodes, providing efficient reuse across multiple calls. This can be expensive as the result can contain O(|E|²) or O(|V|³) tuples depending on graph density.

Calling common_neighbor() without arguments raises a ValueError, to ensure awareness and explicit acknowledgement (full=True) of this cost.

In contrast, common_neighbor(from_=subset) constrains the computation to tuples with the first position in the passed-in subset. The result is not cached; it is specific to the call site. When a significant fraction of the common neighbor relation is needed across a program, common_neighbor(full=True) is typically more efficient. Use common_neighbor(from_=subset) only when small subsets of the common neighbor relationship are needed collectively across the program.

The to parameter can be used together with from_ to further constrain the computation: common_neighbor(from_=subset_a, to=subset_b) computes common neighbors only for node pairs where the first node is in subset_a and the second node is in subset_b. (Since common_neighbor is symmetric in its first two positions, using to without from_ would be functionally redundant, and is not allowed.)

The between parameter provides another way to constrain the computation: Unlike from_ and to, which allow you to independently constrain the first and second positions in common_neighbor tuples to sets of nodes, between allows you to constrain the first and second positions, jointly, to specific pairs of nodes.

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.degree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing the degree of each node.

For directed graphs, a node’s degree is the sum of its indegree and outdegree. For undirected graphs, a self-loop contributes +2 to the node’s degree.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the degree computation: only degrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its degree.

Relationship Schema:

degree(node, node_degree)

  • node (Node): The node.
  • node_degree (Integer): The degree of the node.

Examples:

Undirected Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the degree of each node and inspect
>>> node, node_degree = Node.ref("node"), Integer.ref("node_degree")
>>> degree = graph.degree()
>>> model.select(node.id, node_degree).where(degree(node, node_degree)).inspect()
id node_degree
0 1 1
1 2 3
2 3 3
3 4 1
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute degree of
>>> # Define a subset containing only nodes 2 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>>
>>> # Get degrees only of nodes in the subset
>>> constrained_degree = graph.degree(of=subset)
>>> model.select(node.id, node_degree).where(constrained_degree(node, node_degree)).inspect()
id node_degree
0 2 3
1 3 3

Directed Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define the same nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the degree of each node and inspect
>>> node, node_degree = Node.ref("node"), Integer.ref("node_degree")
>>> degree = graph.degree()
>>> model.select(node.id, node_degree).where(degree(node, node_degree)).inspect()
id node_degree
0 1 1
1 2 3
2 3 3
3 4 1
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute degree of
>>> # Define a subset containing only nodes 1 and 4
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 4)).define(subset(node))
>>>
>>> # Get degrees only of nodes in the subset
>>> constrained_degree = graph.degree(of=subset)
>>> model.select(node.id, node_degree).where(constrained_degree(node, node_degree)).inspect()
id node_degree
0 1 1
1 4 1

Notes:

The degree() method, called with no parameters, computes and caches the full degree relationship, providing efficient reuse across multiple calls to degree(). In contrast, degree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the degree relation is needed across a program, degree() is typically more efficient; this is the typical case. Use degree(of=subset) only when small subsets of the degree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.indegree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing the indegree of each node.

A node’s indegree is the number of incoming edges. For undirected graphs, a node’s indegree is identical to its degree, except self-loops contribute only +1 to a node’s indegree.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the indegree computation: only indegrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its indegree.

Relationship Schema:

indegree(node, node_indegree)

  • node (Node): The node.
  • node_indegree (Integer): The indegree of the node.

Examples:

Undirected Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n3, dst=n4),
... )
>>> # 3. Select the indegree of each node and inspect
>>> node, node_indegree = Node.ref("node"), Integer.ref("node_indegree")
>>> indegree = graph.indegree()
>>> model.select(node.id, node_indegree).where(indegree(node, node_indegree)).inspect()
id node_indegree
0 1 1
1 2 2
2 3 3
3 4 1
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute indegree of
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_indegree = graph.indegree(of=subset)
>>> model.select(node.id, node_indegree).where(constrained_indegree(node, node_indegree)).inspect()
id node_indegree
0 2 2
1 3 3

Directed Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define the same nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n3, dst=n4)
... )
>>>
>>> # 3. Select the indegree of each node and inspect
>>> node, node_indegree = Node.ref("node"), Integer.ref("node_indegree")
>>> indegree = graph.indegree()
>>> model.select(node.id, node_indegree).where(indegree(node, node_indegree)).inspect()
id node_indegree
0 1 0
1 2 1
2 3 2
3 4 1
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute indegree of
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_indegree = graph.indegree(of=subset)
>>> model.select(node.id, node_indegree).where(constrained_indegree(node, node_indegree)).inspect()
id node_indegree
0 2 1
1 3 2

Notes:

The indegree() method, called with no parameters, computes and caches the full indegree relationship, providing efficient reuse across multiple calls to indegree(). In contrast, indegree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the indegree relation is needed across a program, indegree() is typically more efficient; this is the typical case. Use indegree(of=subset) only when small subsets of the indegree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.outdegree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing the outdegree of each node.

A node’s outdegree is the number of outgoing edges. For undirected graphs, a node’s outdegree is identical to its degree, except self-loops contribute only +1 to a node’s outdegree.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the outdegree computation: only outdegrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its outdegree.

Relationship Schema:

outdegree(node, node_outdegree)

  • node (Node): The node.
  • node_outdegree (Integer): The outdegree of the node.

Examples:

Undirected Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the outdegree of each node and inspect
>>> node, node_outdegree = Node.ref("node"), Integer.ref("node_outdegree")
>>> outdegree = graph.outdegree()
>>> model.select(node.id, node_outdegree).where(outdegree(node, node_outdegree)).inspect()
id node_outdegree
0 1 1
1 2 3
2 3 2
3 4 1
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute outdegree of
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_outdegree = graph.outdegree(of=subset)
>>> model.select(node.id, node_outdegree).where(constrained_outdegree(node, node_outdegree)).inspect()
id node_outdegree
0 2 3
1 3 2

Directed Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define the same nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the outdegree of each node and inspect
>>> node, node_outdegree = Node.ref("node"), Integer.ref("node_outdegree")
>>> outdegree = graph.outdegree()
>>> model.select(node.id, node_outdegree).where(outdegree(node, node_outdegree)).inspect()
id node_outdegree
0 1 1
1 2 2
2 3 1
3 4 0
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute outdegree of
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_outdegree = graph.outdegree(of=subset)
>>> model.select(node.id, node_outdegree).where(constrained_outdegree(node, node_outdegree)).inspect()
id node_outdegree
0 2 2
1 3 1

Notes:

The outdegree() method, called with no parameters, computes and caches the full outdegree relationship, providing efficient reuse across multiple calls to outdegree(). In contrast, outdegree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the outdegree relation is needed across a program, outdegree() is typically more efficient; this is the typical case. Use outdegree(of=subset) only when small subsets of the outdegree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.weighted_degree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Returns a binary relationship containing the weighted degree of each node.

A node’s weighted degree is the sum of the weights of all edges connected to it. For directed graphs, this is the sum of the weights of both incoming and outgoing edges. For undirected graphs, the weights of self-loops contribute twice to the node’s weighted degree. For unweighted graphs, all edge weights are considered to be 1.0.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the weighted degree computation: only weighted degrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its weighted degree.

Relationship Schema:

weighted_degree(node, node_weighted_degree)

  • node (Node): The node.
  • node_weighted_degree (Float): The weighted degree of the node.

Examples:

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed, weighted graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n2, dst=n1, weight=0.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... )
>>>
>>> # 3. Select the weighted degree of each node and inspect
>>> node, node_weighted_degree = Node.ref("node"), Float.ref("node_weighted_degree")
>>> weighted_degree = graph.weighted_degree()
>>> model.select(
... node.id, node_weighted_degree
... ).where(
... weighted_degree(node, node_weighted_degree)
... ).inspect()
id node_weighted_degree
0 1 1.0
1 2 2.0
2 3 1.0
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute weighted degree of
>>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_weighted_degree = graph.weighted_degree(of=subset)
>>> model.select(
... node.id, node_weighted_degree
... ).where(
... constrained_weighted_degree(node, node_weighted_degree)
... ).inspect()
id node_weighted_degree
0 2 2.0
1 3 1.0

Notes:

The weighted_degree() method, called with no parameters, computes and caches the full weighted degree relationship, providing efficient reuse across multiple calls to weighted_degree(). In contrast, weighted_degree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the weighted degree relation is needed across a program, weighted_degree() is typically more efficient; this is the typical case. Use weighted_degree(of=subset) only when small subsets of the weighted degree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.weighted_indegree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Returns a binary relationship containing the weighted indegree of each node.

A node’s weighted indegree is the sum of the weights of all incoming edges. For undirected graphs, this is identical to weighted_degree, except for self-loops. For weighted graphs, we assume edge weights are non-negative. For unweighted graphs, all edge weights are considered to be 1.0.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the weighted indegree computation: only weighted indegrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its weighted indegree.

Relationship Schema:

weighted_indegree(node, node_weighted_indegree)

  • node (Node): The node.
  • node_weighted_indegree (Float): The weighted indegree of the node.

Examples:

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed, weighted graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n2, dst=n1, weight=0.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... )
>>>
>>> # 3. Select the weighted indegree of each node and inspect
>>> node, node_weighted_indegree = Node.ref("node"), Float.ref("node_weighted_indegree")
>>> weighted_indegree = graph.weighted_indegree()
>>> model.select(
... node.id, node_weighted_indegree
... ).where(
... weighted_indegree(node, node_weighted_indegree)
... ).inspect()
id node_weighted_indegree
0 1 0.0
1 2 1.0
2 3 1.0
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute weighted indegree of
>>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>> constrained_weighted_indegree = graph.weighted_indegree(of=subset)
>>> model.select(node.id, node_weighted_indegree).where(constrained_weighted_indegree(node, node_weighted_indegree)).inspect()
id node_weighted_indegree
0 2 1.0
1 3 1.0

Notes:

The weighted_indegree() method, called with no parameters, computes and caches the full weighted indegree relationship, providing efficient reuse across multiple calls to weighted_indegree(). In contrast, weighted_indegree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the weighted indegree relation is needed across a program, weighted_indegree() is typically more efficient; this is the typical case. Use weighted_indegree(of=subset) only when small subsets of the weighted indegree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.weighted_outdegree(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Returns a binary relationship containing the weighted outdegree of each node.

A node’s weighted outdegree is the sum of the weights of all outgoing edges. For undirected graphs, this is identical to weighted_degree, except for self-loops. For unweighted graphs, all edge weights are considered to be 1.0.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the weighted outdegree computation: only weighted outdegrees of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its weighted outdegree.

Relationship Schema:

weighted_outdegree(node, node_weighted_outdegree)

  • node (Node): The node.
  • node_weighted_outdegree (Float): The weighted outdegree of the node.

Examples:

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed, weighted graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n2, dst=n1, weight=0.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... )
>>>
>>> # 3. Select the weighted outdegree of each node and inspect
>>> node, node_weighted_outdegree = Node.ref("node"), Float.ref("node_weighted_outdegree")
>>> weighted_outdegree = graph.weighted_outdegree()
>>> model.select(
... node.id, node_weighted_outdegree
... ).where(
... weighted_outdegree(node, node_weighted_outdegree)
... ).inspect()
id node_weighted_outdegree
0 1 1.0
1 2 1.0
2 3 0.0
>>>
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute weighted outdegree of
>>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 2)).define(subset(node))
>>> constrained_weighted_outdegree = graph.weighted_outdegree(of=subset)
>>> model.select(
... node.id, node_weighted_outdegree
... ).where(
... constrained_weighted_outdegree(node, node_weighted_outdegree)
... ).inspect()
id node_weighted_outdegree
0 1 1.0
1 2 1.0

Notes:

The weighted_outdegree() method, called with no parameters, computes and caches the full weighted outdegree relationship, providing efficient reuse across multiple calls to weighted_outdegree(). In contrast, weighted_outdegree(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the weighted outdegree relation is needed across a program, weighted_outdegree() is typically more efficient; this is the typical case. Use weighted_outdegree(of=subset) only when small subsets of the weighted outdegree relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.degree_centrality(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing the degree centrality of each node.

Degree centrality is a measure of a node’s importance, defined as its degree (or weighted degree for weighted graphs) divided by the number of other nodes in the graph.

For unweighted graphs without self-loops, this value will be at most 1.0; unweighted graphs with self-loops might have nodes with a degree centrality greater than 1.0. Weighted graphs may have degree centralities greater than 1.0 as well.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the degree centrality computation: only degree centralities of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its degree centrality.

Relationship Schema:

degree_centrality(node, centrality)

  • node (Node): The node.
  • centrality (Float): The degree centrality of the node.

Examples:

Unweighted Graph Example

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an unweighted graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... )
>>>
>>> # 3. Select the degree centrality of each node and inspect
>>> node, centrality = Node.ref("node"), Float.ref("centrality")
>>> degree_centrality = graph.degree_centrality()
>>> model.select(node.id, centrality).where(degree_centrality(node, centrality)).inspect()
id centrality
0 1 0.333333
1 2 1.000000
2 3 1.000000
3 4 0.333333
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute degree centrality of
>>> # Define a subset containing only nodes 2 and 3
>>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 2, node.id == 3)).define(subset(node))
>>>
>>> # Get degree centralities only of nodes in the subset
>>> constrained_degree_centrality = graph.degree_centrality(of=subset)
>>> model.select(node.id, centrality).where(constrained_degree_centrality(node, centrality)).inspect()
id centrality
0 2 1.0
1 3 1.0

Weighted Graph Example

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a weighted graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=2.0),
... Edge.new(src=n1, dst=n3, weight=0.5),
... Edge.new(src=n2, dst=n3, weight=1.5),
... )
>>>
>>> # 3. Select the degree centrality using weighted degrees
>>> node, centrality = Node.ref("node"), Float.ref("centrality")
>>> degree_centrality = graph.degree_centrality()
>>> model.select(node.id, centrality).where(degree_centrality(node, centrality)).inspect()
id centrality
0 1 1.25
1 2 1.75
2 3 1.00

Notes:

The degree_centrality() method, called with no parameters, computes and caches the full degree centrality relationship, providing efficient reuse across multiple calls to degree_centrality(). In contrast, degree_centrality(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the degree centrality relation is needed across a program, degree_centrality() is typically more efficient; this is the typical case. Use degree_centrality(of=subset) only when small subsets of the degree centrality relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.eigenvector_centrality(
tolerance: float = 1e-06, max_iter: int = 200, *, diagnostic_info: bool = False
) -> Union[Relationship, Tuple[Relationship, Relationship]]
Undirected ✔Directed ✘Weighted ✔

Returns a binary relationship containing the eigenvector centrality of each node.

Parameters:

  • tolerance

    (float, default: 1e-06) - The convergence tolerance for the eigenvector centrality calculation. Must be a non-negative float. Default is 1e-6.
  • max_iter

    (int, default: 200) - The maximum number of iterations to perform in the power method. Default is 200.
  • diagnostic_info

    (bool, default: False) - If True, returns diagnostic information alongside centrality scores. If False (default), returns only centrality scores.

Returns:

  • Relationship or tuple of Relationships - If diagnostic_info is False (default), returns a binary relationship where each tuple represents a node and its eigenvector centrality.

    If diagnostic_info is True, returns a tuple of (eigenvector_centrality, diagnostic_info) where:

    • eigenvector_centrality is the binary relationship described above
    • diagnostic_info is a binary relationship where each tuple contains a diagnostic name string and diagnostic value string.

Raises:

  • DirectedGraphNotSupported - If the graph is directed.

Relationship Schema:

When diagnostic_info=False (default):

eigenvector_centrality(node, centrality)

  • node (Node): The node.
  • centrality (Float): The eigenvector centrality of the node.

When diagnostic_info=True, returns two relationships:

  • eigenvector_centrality(node, centrality) as described above; and
  • diagnostic_info(diagnostic_name, diagnostic_value)
  • diagnostic_name (String): The name of the diagnostic information.
  • diagnostic_value (String): The value of the diagnostic information.

Examples:

Unweighted Graph Example

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an unweighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n4)
... )
>>>
>>> # 3. Select the eigenvector centrality for each node and inspect
>>> node, centrality = Node.ref("node"), Float.ref("centrality")
>>> eigenvector_centrality = graph.eigenvector_centrality()
>>> model.select(node.id, centrality).where(eigenvector_centrality(node, centrality)).inspect()
id centrality
0 1 0.371748
1 2 0.601501
2 3 0.601501
3 4 0.371748

Weighted Graph Example

>>> # 1. Set up a weighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=0.8),
... Edge.new(src=n2, dst=n3, weight=0.7),
... Edge.new(src=n3, dst=n3, weight=2.0),
... Edge.new(src=n2, dst=n4, weight=1.5)
... )
>>>
>>> # 3. Select the eigenvector centrality for each node and inspect
>>> node, centrality = Node.ref("node"), Float.ref("centrality")
>>> eigenvector_centrality = graph.eigenvector_centrality()
>>> model.select(node.id, centrality).where(eigenvector_centrality(node, centrality)).inspect()
id centrality
0 1 0.157327
1 2 0.473250
2 3 0.815024
3 4 0.294988

Diagnostic Information

>>> # Repeat steps 1. and 2. from the previous example.
>>> # 3. Get eigenvector centrality along with diagnostic information
>>> eigenvector_centrality, diag_info = graph.eigenvector_centrality(diagnostic_info=True)
>>> model.select(node.id, centrality).where(eigenvector_centrality(node, centrality)).inspect()
id centrality
0 1 0.157327
1 2 0.473250
2 3 0.815024
3 4 0.294988
>>> model.select(diag_info[0], diag_info[1]).where(diag_info[0] == "Termination status").inspect()
diagnostic_name diagnostic_value
0 Termination status Converged

Notes:

Eigenvector centrality is a measure of the centrality or importance of a node in a graph based on finding the eigenvector associated with the top eigenvalue of the adjacency matrix. We use the power method to compute the eigenvector in our implementation. Note that the power method requires the adjacency matrix to be diagonalizable and will only converge if the absolute value of the top 2 eigenvalues is distinct. Thus, if the graph you are using has an adjacency matrix that is not diagonalizable or the top two eigenvalues are not distinct, this method will not converge. Specifically, bipartite graphs have symmetric spectra, so this computation will not converge. In cases where eigenvector centrality does not converge, consider PageRank.

In the case of weighted graphs, weights are assumed to be non-negative.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.betweenness_centrality(num_pivots: int = 100) -> Relationship
Undirected ✔Directed ✔Weighted ✘

Returns a binary relationship containing the betweenness centrality of each node.

Betweenness centrality measures how important a node is based on how many times that node appears in a shortest path between any two nodes in the graph. Nodes with high betweenness centrality represent bridges between different parts of the graph. For example, in a network representing airports and flights between them, nodes with high betweenness centrality may identify “hub” airports that connect flights to different regions.

Parameters:

  • num_pivots

    (int, default: 100) - The number of pivot nodes to sample as BFS/SSSP sources for approximating betweenness centrality. A larger value yields a more accurate approximation at the cost of increased runtime. When set to |V|, the algorithm reduces to the exact Brandes algorithm. Default is 100.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its betweenness centrality.

Raises:

  • WeightedGraphNotSupported - If the graph is weighted.

Relationship Schema:

betweenness_centrality(node, centrality)

  • node (Node): The node.
  • centrality (Float): The betweenness centrality of the node.

Examples:

>>> from relationalai.semantics import Model, define, select, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Select the betweenness centrality for each node and inspect
>>> node, centrality = Node.ref("node"), Float.ref("centrality")
>>> betweenness_centrality = graph.betweenness_centrality()
>>> model.select(node.id, centrality).where(betweenness_centrality(node, centrality)).inspect()
id centrality
0 1 0.0
1 2 3.0
2 3 0.0
3 4 0.0

Notes:

Calculating betweenness centrality involves computing all of the shortest paths between every pair of nodes in a graph and can be expensive to calculate exactly. The betweenness_centrality relation 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 supports sampling-based approximation via the num_pivots parameter, which controls the number of pivot nodes used in the computation. The default value of 100 pivots yields a time complexity of O(100 * (|V|+|E|)). When the number of pivots equals or exceeds the graph size, the algorithm reduces to the exact Brandes algorithm, with time complexity O(|V|(|V|+|E|)) for unweighted graphs.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.pagerank(
damping_factor: float = 0.85,
tolerance: float = 1e-06,
max_iter: int = 200,
*,
diagnostic_info: bool = False
) -> Union[Relationship, Tuple[Relationship, Relationship]]
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Returns a binary relationship containing the PageRank score of each node.

Parameters:

  • damping_factor

    (float, default: 0.85) - The damping factor for the PageRank calculation. Must be in the range [0, 1). Default is 0.85.
  • tolerance

    (float, default: 1e-06) - The convergence tolerance for the PageRank calculation. Must be a non-negative float. Default is 1e-6.
  • max_iter

    (int, default: 200) - The maximum number of iterations for PageRank to run. Must be a positive integer. Default is 200.
  • diagnostic_info

    (bool, default: False) - If True, returns diagnostic information alongside PageRank scores. If False (default), returns only PageRank scores.

Returns:

  • Relationship or tuple of Relationships - If diagnostic_info is False (default), returns a binary relationship where each tuple represents a node and its PageRank score.

    If diagnostic_info is True, returns a tuple of (pagerank, diagnostic_info) where:

    • pagerank is the binary relationship described above
    • diagnostic_info is a binary relationship where each tuple contains a diagnostic name string and diagnostic value string.

Relationship Schema:

When diagnostic_info=False (default):

pagerank(node, score)

  • node (Node): The node.
  • score (Float): The PageRank score of the node.

When diagnostic_info=True, returns two relationships:

  • pagerank(node, score) as described above; and
  • diagnostic_info(diagnostic_name, diagnostic_value)
  • diagnostic_name (String): The name of the diagnostic information.
  • diagnostic_value (String): The value of the diagnostic information.

Examples:

Unweighted Graph Example

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an unweighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4)
... )
>>>
>>> # 3. Compute PageRank with default parameters and inspect
>>> node, score = Node.ref("node"), Float.ref("score")
>>> pagerank = graph.pagerank()
>>> model.select(node.id, score).where(pagerank(node, score)).inspect()
id score
0 1 0.155788
1 2 0.417488
2 3 0.270936
3 4 0.155788

Weighted Graph Example with Configuration

>>> # 1. Set up a weighted, directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n1, dst=n3, weight=2.0),
... Edge.new(src=n3, dst=n4, weight=3.0)
... )
>>>
>>> # 3. Compute PageRank with custom parameters and inspect
>>> node, score = Node.ref("node"), Float.ref("score")
>>> pagerank = graph.pagerank()
>>> model.select(node.id, score).where(pagerank(node, score)).inspect()
id score
0 1 0.264904
1 2 0.112556
2 3 0.387444
3 4 0.235096

Directed Example with Diagnostics

>>> # 1. Set up graph as above
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n1, dst=n3, weight=2.0),
... Edge.new(src=n3, dst=n4, weight=3.0)
... )
>>>
>>> # 2. Compute PageRank and retrieve results and diagnostic info
>>> node, score = Node.ref("node"), Float.ref("score")
>>> pagerank, diagnostic_info = graph.pagerank(diagnostic_info=True)
>>>
>>> # 3. Select the results
>>> model.select(node.id, score).where(pagerank(node, score)).inspect()
id score
0 1 0.161769
1 2 0.207603
2 3 0.253438
3 4 0.377190
>>>
>>> # 4. Select termination status from diagnostic info
>>> key, value = String.ref("diagnostic_name"), String.ref("diagnostic_value")
>>> model.select(key, value).where(diagnostic_info(key, value), key == "Termination status").inspect()
diagnostic_name diagnostic_value
0 Termination status Converged
>>>
>>> # 5. Select number of iterations from diagnostic info
>>> model.select(key, value).where(diagnostic_info(key, value), key == "Iterations").inspect()
diagnostic_name diagnostic_value
0 Iterations 12
>>>

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.triangle(*, full: Optional[bool] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a ternary relationship containing all triangles in the graph.

Unlike unique_triangle, this relationship contains all permutations of the nodes for each triangle found.

Parameters:

  • full

    (bool, default: None) - If True, computes triangles for all triplets of nodes in the graph. This computation can be expensive for large graphs. Must be set to True to compute the full triangle relationship. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a triangle.

Raises:

  • ValueError - If full is not provided. If full is not True.

Relationship Schema:

triangle(node_a, node_b, node_c)

  • node_a (Node): The first node in the triangle.
  • node_b (Node): The second node in the triangle.
  • node_c (Node): The third node in the triangle.

Examples:

Directed Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph with a 3-cycle
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n1),
... )
>>>
>>> # 3. Select all triangles and inspect
>>> a,b,c = Node.ref("a"), Node.ref("b"), Node.ref("c")
>>> triangle = graph.triangle(full=True)
>>> model.select(a.id, b.id, c.id).where(triangle(a, b, c)).inspect()
id id_2 id_3
0 1 2 3
1 2 3 1
2 3 1 2

Undirected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph with a triangle
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define the same nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n1),
... )
>>>
>>> # 3. Select all triangles and inspect
>>> a,b,c = Node.ref("a"), Node.ref("b"), Node.ref("c")
>>> triangle = graph.triangle(full=True)
>>> model.select(a.id, b.id, c.id).where(triangle(a, b, c)).inspect()
id id_2 id_3
0 1 2 3
1 1 3 2
2 2 1 3
3 2 3 1
4 3 1 2
5 3 2 1

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.unique_triangle(*, full: Optional[bool] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a ternary relationship containing all unique triangles in the graph.

Parameters:

  • full

    (bool, default: None) - If True, computes unique triangles for all triplets of nodes in the graph. This computation can be expensive for large graphs. Must be set to True to compute the full unique_triangle relationship. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a unique triangle.

Raises:

  • ValueError - If full is not provided. If full is not True.

Relationship Schema:

unique_triangle(node_a, node_b, node_c)

  • node_a (Node): The first node in the triangle.
  • node_b (Node): The second node in the triangle.
  • node_c (Node): The third node in the triangle.

Examples:

Directed Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n3),
... Edge.new(src=n2, dst=n1),
... Edge.new(src=n3, dst=n2),
... Edge.new(src=n3, dst=n4),
... Edge.new(src=n2, dst=n4),
... )
>>>
>>> # 3. Select the unique triangles and inspect
>>> a,b,c = Node.ref("a"), Node.ref("b"), Node.ref("c")
>>> unique_triangle = graph.unique_triangle(full=True)
>>> model.select(a.id, b.id, c.id).where(unique_triangle(a, b, c)).inspect()
id id_2 id_3
0 3 2 1

Undirected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges forming two triangles
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n1), # Forms triangle (1,2,3)
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n3, dst=n4), # Forms triangle (2,3,4)
... )
>>>
>>> # 3. Select the unique triangles and inspect
>>> a,b,c = Node.ref("a"), Node.ref("b"), Node.ref("c")
>>> unique_triangle = graph.unique_triangle(full=True)
>>> model.select(a.id, b.id, c.id).where(unique_triangle(a, b, c)).inspect()
id id_2 id_3
0 3 1 2
1 4 3 2

Notes:

This relationship contains triples of nodes (a, b, c) representing unique triangles.

For undirected graphs, uniqueness of each triangle is guaranteed because the relationship only contains triples where a < b < c.

For directed graphs, the triple (a, b, c) represents a triangle with directed edges (a, b), (b, c), and (c, a), and is unique up to ordering of the nodes and direction of the edges. unique_triangle only contains triples such that a < b, a < c, and b != c. For example, the triples (1, 2, 3) and (1, 3, 2) represent two unique directed triangles.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.num_triangles() -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a unary relationship containing the total number of unique triangles in the graph.

This method counts the number of unique triangles as defined by the unique_triangle relationship, which has different uniqueness constraints for directed and undirected graphs.

Returns:

  • Relationship - A unary relationship containing the total number of unique triangles in the graph.

Relationship Schema:

num_triangles(count)

  • count (Integer): The total number of unique triangles in the graph.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n3),
... Edge.new(src=n2, dst=n1),
... Edge.new(src=n3, dst=n2),
... Edge.new(src=n3, dst=n4),
... Edge.new(src=n1, dst=n4),
... )
>>>
>>> # 3. Inspect the number of unique triangles
>>> graph.num_triangles().inspect()
num_triangles
0 1

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.triangle_count(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship containing the number of unique triangles each node belongs to.

A triangle is a set of three nodes where each node has a directed or undirected edge to the other two nodes, forming a 3-cycle.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the triangle count computation: only triangle counts of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and the number of unique triangles it is a part of.

Relationship Schema:

triangle_count(node, count)

  • node (Node): The node.
  • count (Integer): The number of unique triangles the node belongs to.

Examples:

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5 = [Node.new(id=i) for i in range(1, 6)]
>>> model.define(n1, n2, n3, n4, n5)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n3, dst=n1),
... Edge.new(src=n3, dst=n4),
... Edge.new(src=n5, dst=n1),
... )
>>>
>>> # 3. Select the triangle count for each node and inspect
>>> node, count = Node.ref("node"), Integer.ref("count")
>>> triangle_count = graph.triangle_count()
>>> model.select(node.id, count).where(triangle_count(node, count)).inspect()
id count
0 1 1
1 2 1
2 3 1
3 4 0
4 5 0
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute triangle counts of
>>> # Define a subset containing only nodes 1 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 3)).define(subset(node))
>>>
>>> # Get triangle counts only of nodes in the subset
>>> constrained_triangle_count = graph.triangle_count(of=subset)
>>> model.select(node.id, count).where(constrained_triangle_count(node, count)).inspect()
id count
0 1 1
1 3 1

Notes:

The triangle_count() method, called with no parameters, computes and caches the full triangle count relationship, providing efficient reuse across multiple calls to triangle_count(). In contrast, triangle_count(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the triangle count relation is needed across a program, triangle_count() is typically more efficient; this is the typical case. Use triangle_count(of=subset) only when small subsets of the triangle count relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.local_clustering_coefficient(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✘Weighted ✔

Returns a binary relationship containing the local clustering coefficient of each node.

The local clustering coefficient quantifies how close a node’s neighbors are to forming a clique (a complete subgraph). The coefficient ranges from 0.0 to 1.0, where 0.0 indicates none of the neighbors have edges directly connecting them, and 1.0 indicates all neighbors have edges directly connecting them.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the local clustering coefficient computation: only coefficients of nodes in this relationship are computed and returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and its local clustering coefficient.

Raises:

  • NotImplementedError - If the graph is directed.

Relationship Schema:

local_clustering_coefficient(node, coefficient)

  • node (Node): The node.
  • coefficient (Float): The local clustering coefficient of the node.

Examples:

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5 = [Node.new(id=i) for i in range(1, 6)]
>>> model.define(n1, n2, n3, n4, n5)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n1, dst=n3),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n4),
... Edge.new(src=n4, dst=n5),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n3, dst=n5),
... )
>>>
>>> # 3. Select the local clustering coefficient for each node
>>> node, coeff = Node.ref("node"), Float.ref("coeff")
>>> lcc = graph.local_clustering_coefficient()
>>> model.select(node.id, coeff).where(lcc(node, coeff)).inspect()
id coeff
0 1 1.000000
1 2 0.666667
2 3 0.500000
3 4 0.666667
4 5 1.000000
>>> # 4. Use 'of' parameter to constrain the set of nodes to compute local clustering coefficients of
>>> # Define a subset containing only nodes 1 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 3)).define(subset(node))
>>>
>>> # Get local clustering coefficients only of nodes in the subset
>>> constrained_lcc = graph.local_clustering_coefficient(of=subset)
>>> model.select(node.id, coeff).where(constrained_lcc(node, coeff)).inspect()
id coeff
0 1 1.0
1 3 0.5

Notes:

The local clustering coefficient for node v is::

(2 * num_neighbor_edges(v)) / (ext_degree(v) * (ext_degree(v) - 1))

where num_neighbor_edges(v) is the number of edges between the neighbors of node v, and ext_degree(v) is the degree of the node excluding self-loops. If ext_degree(v) is less than 2, the local clustering coefficient is 0.0.

The local_clustering_coefficient() method, called with no parameters, computes and caches the full local clustering coefficient relationship, providing efficient reuse across multiple calls to local_clustering_coefficient(). In contrast, local_clustering_coefficient(of=subset) computes a constrained relationship specific to the passed-in subset and that call site. When a significant fraction of the local clustering coefficient relation is needed across a program, local_clustering_coefficient() is typically more efficient; this is the typical case. Use local_clustering_coefficient(of=subset) only when small subsets of the local clustering coefficient relationship are needed collectively across the program.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.average_clustering_coefficient() -> Relationship
Undirected ✔Directed ✘Weighted ✔

Returns a unary relationship containing the average clustering coefficient of the graph.

The average clustering coefficient is the average of the local clustering coefficients of the nodes in a graph.

Returns:

  • Relationship - A unary relationship containing the average clustering coefficient of the graph.

Raises:

  • NotImplementedError - If the graph is directed.

Relationship Schema:

average_clustering_coefficient(coefficient)

  • coefficient (Float): The average clustering coefficient of the graph.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5 = [Node.new(id=i) for i in range(1, 6)]
>>> model.define(n1, n2, n3, n4, n5)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n1, dst=n3),
... Edge.new(src=n1, dst=n4),
... Edge.new(src=n1, dst=n5),
... Edge.new(src=n2, dst=n3),
... )
>>>
>>> # 3. Inspect the average clustering coefficient
>>> graph.average_clustering_coefficient().inspect()
coefficient
0 0.433333

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.reachable(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship of pairs of nodes (u, v) where v is reachable from u.

A node v is considered reachable from a node u if there is a path of edges from u to v.

Parameters:

  • full

    (bool, default: None) - If True, computes reachability for all pairs of nodes in the graph. This computation can be expensive for large graphs. Must be set to True to compute the full reachable relationship. Cannot be used with other parameters.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the first position of the reachable computation: only reachability from nodes in this relationship are returned. Cannot be used with other parameters.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the second position of the reachable computation: only reachability to nodes in this relationship are returned. Cannot be used with other parameters.
  • between

    (Relationship, default: None) - Not yet supported for reachable. If provided, raises an error. If you need this capability, please reach out.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and a node that is reachable from it.

Relationship Schema:

reachable(from_node, to_node)

  • from_node (Node): The node from which the path originates.
  • to_node (Node): The node that is reachable from the first node.

Examples:

Directed Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... )
>>>
>>> # 3. Select all reachable pairs and inspect
>>> from_node, to_node = Node.ref("start"), Node.ref("end")
>>> reachable = graph.reachable(full=True)
>>> model.select(from_node.id, to_node.id).where(reachable(from_node, to_node)).inspect()
id id_2
0 1 1
1 1 2
2 2 2
3 3 2
4 3 3
>>> # 4. Use 'from_' parameter to get reachability from specific nodes
>>> # Define a subset containing nodes 1 and 3
>>> subset = model.Relationship(f"{Node:node} is in from subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 3)).define(subset(node))
>>>
>>> # Get reachability from nodes in the subset to all other nodes
>>> reachable_from = graph.reachable(from_=subset)
>>> model.select(from_node.id, to_node.id).where(
... reachable_from(from_node, to_node)
... ).inspect()
id id_2
0 1 1
1 1 2
2 3 2
3 3 3
>>> # 5. Use 'to' parameter to get reachability to specific nodes
>>> # Define a different subset containing node 2
>>> to_subset = model.Relationship(f"{Node:node} is in to subset")
>>> node = Node.ref()
>>> model.where(node.id == 2).define(to_subset(node))
>>>
>>> # Get reachability from all nodes to node 2
>>> reachable_to = graph.reachable(to=to_subset)
>>> model.select(from_node.id, to_node.id).where(
... reachable_to(from_node, to_node)
... ).inspect()
id id_2
0 1 2
1 2 2
2 3 2

Undirected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define the same nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... )
>>>
>>> # 3. Select all reachable pairs and inspect
>>> from_node, to_node = Node.ref("start"), Node.ref("end")
>>> reachable = graph.reachable(full=True)
>>> model.select(from_node.id, to_node.id).where(reachable(from_node, to_node)).inspect()
id id_2
0 1 1
1 1 2
2 1 3
3 2 1
4 2 2
5 2 3
6 3 1
7 3 2
8 3 3

Notes:

The reachable(full=True) method computes and caches the full reachable relationship, providing efficient reuse across multiple calls. In contrast, reachable(from_=subset) or reachable(to=subset) compute constrained relationships specific to the passed-in subset and call site. When a significant fraction of the reachable relation is needed, reachable(full=True) is typically more efficient. Use constrained variants only when small subsets are needed.

In directed graphs, the reachable relationship is asymmetric: node B may be reachable from node A without node A being reachable from node B. This asymmetry means that from_ and to parameters have distinct, non-interchangeable meanings.

Important: Simultaneous use of from_ and to parameters is not yet supported. The between parameter is also not yet supported. If you need these capabilities, please reach out.

There is a slight difference between transitive closure and reachable. The transitive closure of a binary relation E is the smallest relation that contains E and is transitive. When E is the edge set of a graph, the transitive closure of E does not include (u, u) if u is isolated. reachable is a different binary relation in which any node u is always reachable from u. In particular, transitive closure is a more general concept than reachable.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.distance(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a ternary relationship containing the shortest path length between pairs of nodes.

This method computes the shortest path length between all pairs of reachable nodes. The calculation depends on whether the graph is weighted:

  • For unweighted graphs, the length is the number of edges in the shortest path.
  • For weighted graphs, the length is the sum of edge weights along the shortest path. Edge weights are assumed to be non-negative.

Parameters:

  • full

    (bool, default: None) - If True, computes distances for all pairs of nodes in the graph. This computation can be expensive for large graphs. Must be set to True to compute the full distance relationship. Cannot be used with other parameters.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the first position (start_node) of the distance computation: only distances from nodes in this relationship are returned. Cannot be used with other parameters.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the second position (end_node) of the distance computation: only distances to nodes in this relationship are returned. Cannot be used with other parameters.
  • between

    (Relationship, default: None) - Not yet supported for distance. If provided, raises an error. If you need this capability, please reach out.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and the shortest path length between them.

Relationship Schema:

distance(start_node, end_node, length)

  • start_node (Node): The start node of the path.
  • end_node (Node): The end node of the path.
  • length (Integer or Float): The shortest path length, returned as an Integer for unweighted graphs and a Float for weighted graphs.

Examples:

Unweighted Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an unweighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... )
>>>
>>> # 3. Select the shortest path length between all pairs of nodes
>>> start, end = Node.ref("start"), Node.ref("end")
>>> length = Integer.ref("length")
>>> dist = graph.distance(full=True)
>>> model.select(start.id, end.id, length).where(dist(start, end, length)).inspect()
id id_2 length
0 1 1 0
1 1 2 1
2 1 3 2
3 1 4 2
4 2 1 1
5 2 2 0
6 2 3 1
7 2 4 1
8 3 1 2
9 3 2 1
10 3 3 0
11 3 4 2
12 4 1 2
13 4 2 1
14 4 3 2
15 4 4 0
>>> # 4. Use 'from_' parameter to get distances from specific nodes
>>> # Define a subset containing nodes 1 and 3
>>> subset = model.Relationship(f"{Node:node} is in from subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 3)).define(subset(node))
>>>
>>> # Get distances from nodes in the subset to all other nodes
>>> dist_from = graph.distance(from_=subset)
>>> model.select(start.id, end.id, length).where(
... dist_from(start, end, length)
... ).inspect()
id id_2 length
0 1 1 0
1 1 2 1
2 1 3 2
3 1 4 2
4 3 1 2
5 3 2 1
6 3 3 0
7 3 4 2
>>> # 5. Use 'to' parameter to get distances to specific nodes
>>> # Define a different subset containing node 4
>>> to_subset = model.Relationship(f"{Node:node} is in to subset")
>>> node = Node.ref()
>>> model.where(node.id == 4).define(to_subset(node))
>>>
>>> # Get distances from all nodes to node 4
>>> dist_to = graph.distance(to=to_subset)
>>> model.select(start.id, end.id, length).where(
... dist_to(start, end, length)
... ).inspect()
id id_2 length
0 1 4 2
1 2 4 1
2 3 4 2
3 4 4 0

Weighted Graph Example

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a weighted, directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=2.0),
... Edge.new(src=n1, dst=n3, weight=0.5),
... Edge.new(src=n2, dst=n1, weight=1.5),
... )
>>>
>>> # 3. Select the shortest path length between all pairs of nodes
>>> start, end = Node.ref("start"), Node.ref("end")
>>> length = Float.ref("length")
>>> dist = graph.distance(full=True)
>>> model.select(start.id, end.id, length).where(dist(start, end, length)).inspect()
id id_2 length
0 1 1 0.0
1 1 2 2.0
2 1 3 0.5
3 2 1 1.5
4 2 2 0.0
5 2 3 2.0
6 3 3 0.0

Notes:

The distance(full=True) method computes and caches the full distance relationship, providing efficient reuse across multiple calls. In contrast, distance(from_=subset) or distance(to=subset) compute constrained relationships specific to the passed-in subset and call site. When a significant fraction of the distance relation is needed, distance(full=True) is typically more efficient. Use constrained variants only when small subsets are needed.

In directed graphs, the distance relationship is asymmetric: the distance from node A to node B may differ from the distance from node B to node A. This asymmetry means that from_ and to parameters have distinct, non-interchangeable meanings.

Important: Simultaneous use of from_ and to parameters is not yet supported. The between parameter is also not yet supported. If you need these capabilities, please reach out.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.weakly_connected_component(*, of: Optional[Relationship] = None) -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a binary relationship that maps each node to its weakly connected component.

A weakly connected component is a subgraph where for every pair of nodes, there is a path between them in the underlying undirected graph. For undirected graphs, this is equivalent to finding the connected components.

Parameters:

  • of

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the weakly connected component computation: only component memberships of nodes in this relationship are returned.

Returns:

  • Relationship - A binary relationship where each tuple represents a node and the identifier of the component it belongs to.

Relationship Schema:

weakly_connected_component(node, component_id)

  • node (Node): The node.
  • component_id (Node): The identifier for the component.

Examples:

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
>>> model.define(n1, n2, n3)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... )
>>>
>>> # 3. Select the component ID for each node and inspect
>>> node, component_id = Node.ref("node"), Node.ref("component_id")
>>> wcc = graph.weakly_connected_component()
>>> model.select(node.id, component_id.id).where(wcc(node, component_id)).inspect()
id id_2
0 1 3
1 2 3
2 3 3
>>> # 4. Use 'of' parameter to constrain computation to subset of nodes
>>> # Define a subset containing only nodes 1 and 3
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(model.union(node.id == 1, node.id == 3)).define(subset(node))
>>>
>>> # Get component membership only for nodes in the subset
>>> constrained_wcc = graph.weakly_connected_component(of=subset)
>>> model.select(node.id, component_id.id).where(
... constrained_wcc(node, component_id)
... ).inspect()
id id_2
0 1 3
1 3 3

Notes:

The component_id is the node with the minimum ID within each component.

The weakly_connected_component() method, called with no parameters, computes and caches the full weakly connected component relationship, providing efficient reuse across multiple calls to weakly_connected_component().

In contrast, weakly_connected_component(of=subset) computes a constrained relationship specific to the passed-in subset and that call site.

Note that the constrained computation requires working over all nodes in the components containing the nodes in subset. When that set of nodes constitutes an appreciable fraction of the graph, the constrained computation may be less efficient than computing the full relationship. Use weakly_connected_component(of=subset) only when small subsets of the weakly connected component relationship are needed collectively across the program, and the associated components cover only a small part of the graph.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.diameter_range() -> Tuple[Relationship, Relationship]
Undirected ✔Directed ✔Weighted ✔

Estimates the graph diameter and returns it as a minimum and maximum bound.

Returns:

  • (Relationship, Relationship) - A tuple containing two unary Relationship objects: (min_bound, max_bound).

    • min_bound: A relationship of the form min_bound(value) where value (Integer) is the lower bound of the estimated diameter.
    • max_bound: A relationship of the form max_bound(value) where value (Integer) is the upper bound of the estimated diameter.

Relationship Schema:

min_bound(value)

  • value (Integer): The lower bound of the estimated diameter.

max_bound(value)

  • value (Integer): The upper bound of the estimated diameter.

Examples:

Connected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a connected, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Get the diameter range and inspect each bound
>>> min_bound, max_bound = graph.diameter_range()
>>> min_bound.inspect()
value
0 3
>>> max_bound.inspect()
value
0 4

Disconnected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a disconnected, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5 = [Node.new(id=i) for i in range(1, 6)]
>>> model.define(n1, n2, n3, n4, n5)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n4),
... Edge.new(src=n4, dst=n5),
... )
>>>
>>> # 3. The range reflects the largest component {3, 4, 5}
>>> min_bound, max_bound = graph.diameter_range()
>>> min_bound.inspect()
value
0 2
>>> max_bound.inspect()
value
0 2

Notes:

This method is used to determine the range of possible diameter values in the graph. This is done by selecting a number of random nodes in the graph and taking the maximum of all shortest paths lengths to the rest of the graph. This gives a range per node. Then, the intersection of these ranges is taken and the final range is returned.

For disconnected graphs, diameter_range returns the diameter range of the largest (weakly) connected component. This behavior deviates from many graph theory resources, which typically define the diameter of a disconnected graph as infinity.

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.is_connected() -> Relationship
Undirected ✔Directed ✔Weighted ✔

Returns a unary relationship containing whether the graph is connected.

A graph is considered connected if every node is reachable from every other node in the underlying undirected graph.

Returns:

  • Relationship - A unary relationship containing a boolean indicator of whether the graph is connected.

Relationship Schema:

is_connected(connected)

  • connected (Boolean): Whether the graph is connected.

Examples:

Connected Graph Example

>>> from relationalai.semantics import Model
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a connected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Select and inspect the relation
>>> model.select(graph.is_connected()).inspect()
is_connected
0 True

Disconnected Graph Example

>>> from relationalai.semantics import Model, define, select
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a disconnected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5 = [Node.new(id=i) for i in range(1, 6)]
>>> model.define(n1, n2, n3, n4, n5)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n3, dst=n2),
... Edge.new(src=n4, dst=n5), # This edge creates a separate component
... )
>>>
>>> # 3. Select and inspect the relation
>>> model.select(graph.is_connected()).inspect()
is_connected
0 False

Notes:

An empty graph is considered connected.

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.jaccard_similarity(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✘Weighted ✔Unweighted ✔

Returns a ternary relationship containing the Jaccard similarity for pairs of nodes.

The Jaccard similarity is a measure between two nodes that ranges from 0.0 to 1.0, where higher values indicate greater similarity.

Parameters:

  • full

    (bool, default: None) - If True, computes the Jaccard similarity for all pairs of nodes in the graph. This computation can be expensive for large graphs, as the result can scale quadratically in the number of nodes. Mutually exclusive with other parameters. Default is None.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the Jaccard similarity computation: only Jaccard similarity scores for node pairs where the first node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. Can only be used together with the from_ parameter. When provided with from_, constrains the domain of the Jaccard similarity computation: only Jaccard similarity scores for node pairs where the first node is in from_ and the second node is in to are computed and returned. Default is None.
  • between

    (Relationship, default: None) - A binary relationship containing pairs of nodes. When provided, constrains the domain of the Jaccard similarity computation: only Jaccard similarity scores for the specific node pairs in this relationship are computed and returned. Mutually exclusive with other parameters. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and their Jaccard similarity.

Raises:

  • DirectedGraphNotSupported - If the graph is directed.
  • TypeError - If from_, to, or between is not a Relationship.
  • ValueError - If full is provided with any other parameter. If between is provided with any other parameter. If from_ is provided with any parameter other than to. If to is provided without the from_ parameter. If none of full, from_, or between is provided. If full is not True or None. If from_, to, or between is not attached to the same model as the graph. If from_, to, or between does not contain the graph’s Node concept. If from_ or to is not a unary relationship. If between is not a binary relationship.

Relationship Schema:

jaccard_similarity(node_u, node_v, score)

  • node_u (Node): The first node in the pair.
  • node_v (Node): The second node in the pair.
  • score (Float): The Jaccard similarity of the pair.

Examples:

Unweighted Graph Examples

Undirected Graph

>>> from relationalai.semantics import Model, define, select, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>> u, v, score = Node.ref("u"), Node.ref("v"), Float.ref("score")
>>> jaccard_similarity = graph.jaccard_similarity(full=True)
>>> model.select(score).where(jaccard_similarity(u, v, score), u.id == 2, v.id == 4).inspect()
score
0 0.25

Weighted Graph Example

>>> from relationalai.semantics import Model, define, select, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a weighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges with weights
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.6),
... Edge.new(src=n1, dst=n3, weight=1.4),
... Edge.new(src=n2, dst=n3, weight=0.46),
... Edge.new(src=n3, dst=n4, weight=2.5),
... )
>>>
>>> # 3. Select the weighted Jaccard similarity for the pair (1, 2)
>>> u, v, score = Node.ref("u"), Node.ref("v"), Float.ref("score")
>>> jaccard_similarity = graph.jaccard_similarity(full=True)
>>> model.select(score).where(jaccard_similarity(u, v, score), u.id == 1, v.id == 2).inspect()
score
0 0.1

Domain Constraint Examples

>>> from relationalai.semantics import where
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>> u, v, score = Node.ref("u"), Node.ref("v"), Float.ref("score")
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 2).define(subset(node))
>>>
>>> # Get Jaccard similarity scores only for pairs where first node is in subset
>>> # Use 'from_' parameter to constrain the set of nodes for the first position
>>> # Using the same undirected unweighted graph from above
>>> constrained_jaccard_similarity = graph.jaccard_similarity(from_=subset)
>>> model.select(u.id, v.id, score).where(constrained_jaccard_similarity(u, v, score)).inspect()
id id_2 score
0 2 2 1.00
1 2 3 0.50
2 2 4 0.25
>>> # Use both 'from_' and 'to' parameters to constrain both positions
>>> from_subset = model.Relationship(f"{Node:node} is in from_subset")
>>> to_subset = model.Relationship(f"{Node:node} is in to_subset")
>>> model.where(node.id == 2).define(from_subset(node))
>>> model.where(node.id == 4).define(to_subset(node))
>>>
>>> # Get Jaccard similarity scores only where first node is in from_subset and second node is in to_subset
>>> constrained_jaccard_similarity = graph.jaccard_similarity(from_=from_subset, to=to_subset)
>>> model.select(u.id, v.id, score).where(constrained_jaccard_similarity(u, v, score)).inspect()
id id_2 score
0 2 4 0.25
>>> # Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 2, node_b.id == 4).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 3, node_b.id == 4).define(pairs(node_a, node_b))
>>>
>>> # Get Jaccard similarity scores only for the specific pairs (2, 4) and (3, 4)
>>> constrained_jaccard_similarity = graph.jaccard_similarity(between=pairs)
>>> model.select(u.id, v.id, score).where(constrained_jaccard_similarity(u, v, score)).inspect()
id id_2 score
0 2 4 0.250000
1 3 4 0.666667

Notes:

The unweighted Jaccard similarity between two nodes is the ratio of the size of the intersection to the size of the union of their neighbors.

The weighted Jaccard similarity considers the weights of the edges. The definition used here is taken from the reference noted below. It is the ratio between two quantities:

  1. Numerator: For every other node w in the graph, find the minimum of the edge weights (u, w) and (v, w), and sum these minimums.
  2. Denominator: For every other node w in the graph, find the maximum of the edge weights (u, w) and (v, w), and sum these maximums.

If an edge does not exist, its weight is considered 0.0. This can be better understood via the following calculation for the weighted example below.

node idedge weights to node 1edge weights to node 2minmax
10.01.60.01.6
21.60.00.01.6
31.40.460.461.4
40.00.00.00.0

The weighted Jaccard similarity between node 1 and 2 is then: 0.46 / (1.6 + 1.6 + 1.4) = 0.1.

Edge weights are assumed to be non-negative, so the neighborhood vectors contain only non-negative elements. Therefore, the Jaccard similarity score is always between 0.0 and 1.0, inclusive.

The jaccard_similarity(full=True) method computes and caches the full Jaccard similarity relationship for all pairs of nodes, providing efficient reuse across multiple calls. This can be expensive as the result can contain O(|V|²) tuples.

Calling jaccard_similarity() without arguments raises a ValueError, to ensure awareness and explicit acknowledgement (full=True) of this cost.

In contrast, jaccard_similarity(from_=subset) constrains the computation to tuples with the first position in the passed-in subset. The result is not cached; it is specific to the call site. When a significant fraction of the Jaccard similarity relation is needed across a program, jaccard_similarity(full=True) is typically more efficient. Use jaccard_similarity(from_=subset) only when small subsets of the Jaccard similarity relationship are needed collectively across the program.

The to parameter can be used together with from_ to further constrain the computation: jaccard_similarity(from_=subset_a, to=subset_b) computes Jaccard similarity scores only for node pairs where the first node is in subset_a and the second node is in subset_b. (Since jaccard_similarity is symmetric in its first two positions, using to without from_ would be functionally redundant, and is not allowed.)

The between parameter provides another way to constrain the computation. Unlike from_ and to, which allow you to independently constrain the first and second positions in jaccard_similarity tuples to sets of nodes, between allows you constrain the first and second positions, jointly, to specific pairs of nodes.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.cosine_similarity(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✘Weighted ✔Unweighted ✔

Returns a ternary relationship containing the cosine similarity for pairs of nodes.

The cosine similarity measures the similarity between two nodes based on the angle between their neighborhood vectors. The score ranges from 0.0 to 1.0, inclusive, where 1.0 indicates identical sets of neighbors.

Parameters:

  • full

    (bool, default: None) - If True, computes the cosine similarity for all pairs of nodes in the graph. This computation can be expensive for large graphs, as the result can scale quadratically in the number of nodes. Mutually exclusive with other parameters. Default is None.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the cosine similarity computation: only cosine similarity scores for node pairs where the first node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. Can only be used together with the from_ parameter. When provided with from_, constrains the domain of the cosine similarity computation: only cosine similarity scores for node pairs where the first node is in from_ and the second node is in to are computed and returned. Default is None.
  • between

    (Relationship, default: None) - A binary relationship containing pairs of nodes. When provided, constrains the domain of the cosine similarity computation: only cosine similarity scores for the specific node pairs in this relationship are computed and returned. Mutually exclusive with other parameters. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and their cosine similarity.

Raises:

  • DirectedGraphNotSupported - If the graph is directed.
  • TypeError - If from_, to, or between is not a Relationship.
  • ValueError - If full is provided with any other parameter. If between is provided with any other parameter. If from_ is provided with any parameter other than to. If to is provided without the from_ parameter. If none of full, from_, or between is provided. If full is not True or None. If from_, to, or between is not attached to the same model as the graph. If from_, to, or between does not contain the graph’s Node concept. If from_ or to is not a unary relationship. If between is not a binary relationship.

Relationship Schema:

cosine_similarity(node_u, node_v, score)

  • node_u (Node): The first node in the pair.
  • node_v (Node): The second node in the pair.
  • score (Float): The cosine similarity of the pair.

Examples:

Unweighted Graph Examples

Undirected Graph

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>> u, v, score = Node.ref("u"), Node.ref("v"), Float.ref("score")
>>> cosine_similarity = graph.cosine_similarity(full=True)
>>> model.select(score).where(cosine_similarity(u, v, score), u.id == 2, v.id == 4).inspect()
score
0 0.408248

Weighted Graph Examples

Undirected Graph

>>> from relationalai.semantics import Model, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>> n1, n2, n3, n4, n13, n14 = [Node.new(id=i) for i in [1, 2, 3, 4, 13, 14]]
>>> model.define(n1, n2, n3, n4, n13, n14)
>>> model.define(
... Edge.new(src=n1, dst=n2, weight=1.6),
... Edge.new(src=n1, dst=n3, weight=1.4),
... Edge.new(src=n2, dst=n3, weight=1.2),
... Edge.new(src=n3, dst=n4, weight=2.5),
... Edge.new(src=n14, dst=n13, weight=1.0),
... )
>>> u, v, score = Node.ref("u"), Node.ref("v"), Float.ref("score")
>>> cosine_similarity = graph.cosine_similarity(full=True)
>>> model.select(score).where(cosine_similarity(u, v, score), u.id == 1, v.id == 2).inspect()
score
0 0.395103

Domain Constraint Examples

>>> # Use 'from_' parameter to constrain the set of nodes for the first position
>>> # Using the same undirected weighted graph from above
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 2).define(subset(node))
>>>
>>> # Get cosine similarity scores only for pairs where first node is in subset
>>> constrained_cosine_similarity = graph.cosine_similarity(from_=subset)
>>> model.select(u.id, v.id, score).where(constrained_cosine_similarity(u, v, score)).inspect()
id id_2 score
0 2 1 0.395103
1 2 2 1.000000
2 2 3 0.360541
3 2 4 0.600000
>>> # Use both 'from_' and 'to' parameters to constrain both positions
>>> from_subset = model.Relationship(f"{Node:node} is in from_subset")
>>> to_subset = model.Relationship(f"{Node:node} is in to_subset")
>>> model.where(node.id == 2).define(from_subset(node))
>>> model.where(node.id == 1).define(to_subset(node))
>>>
>>> # Get cosine similarity scores only where first node is in from_subset and second node is in to_subset
>>> constrained_cosine_similarity = graph.cosine_similarity(from_=from_subset, to=to_subset)
>>> model.select(u.id, v.id, score).where(constrained_cosine_similarity(u, v, score)).inspect()
id id_2 score
0 2 1 0.395103
>>> # Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 1, node_b.id == 2).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 1, node_b.id == 1).define(pairs(node_a, node_b))
>>>
>>> # Get cosine similarity scores only for the specific pairs (2, 4) and (3, 4)
>>> constrained_cosine_similarity = graph.cosine_similarity(between=pairs)
>>> model.select(u.id, v.id, score).where(constrained_cosine_similarity(u, v, score)).inspect()
id id_2 score
0 1 1 1.000000
1 1 2 0.395103

Notes:

The cosine similarity is defined as the normalized inner product of two vectors representing the neighborhoods of the nodes u and v.

  • For unweighted graphs, the vector for a node u contains a 1 for each neighbor and a 0 for each non-neighbor.
  • For weighted graphs, the vector for a node u contains the edge weight for each neighbor and a 0 for each non-neighbor.

Edge weights are assumed to be non-negative, so the neighborhood vectors contain only non-negative elements. Therefore, the cosine similarity score is always between 0.0 and 1.0, inclusive.

The cosine_similarity(full=True) method computes and caches the full cosine similarity relationship for all pairs of nodes, providing efficient reuse across multiple calls. This can be expensive as the result can contain O(|V|²) tuples.

Calling cosine_similarity() without arguments raises a ValueError, to ensure awareness and explicit acknowledgement (full=True) of this cost.

In contrast, cosine_similarity(from_=subset) constrains the computation to tuples with the first position in the passed-in subset. The result is not cached; it is specific to the call site. When a significant fraction of the cosine similarity relation is needed across a program, cosine_similarity(full=True) is typically more efficient. Use cosine_similarity(from_=subset) only when small subsets of the cosine similarity relationship are needed collectively across the program.

The to parameter can be used together with from_ to further constrain the computation: cosine_similarity(from_=subset_a, to=subset_b) computes cosine similarity scores only for node pairs where the first node is in subset_a and the second node is in subset_b. (Since cosine_similarity is symmetric in its first two positions, using to without from_ would be functionally redundant, and is not allowed.)

The between parameter provides another way to constrain the computation. Unlike from_ and to, which allow you to independently constrain the first and second positions in cosine_similarity tuples to sets of nodes, between allows you constrain the first and second positions, jointly, to specific pairs of nodes.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.adamic_adar(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✘Weighted ✘

Returns a ternary relationship containing the Adamic-Adar index for pairs of nodes.

The Adamic-Adar index is a similarity measure between two nodes based on the amount of shared neighbors between them, giving more weight to common neighbors that are less connected.

Parameters:

  • full

    (bool, default: None) - If True, computes the Adamic-Adar index for all pairs of nodes in the graph. This computation can be expensive for large graphs, as dependencies can scale quadratically in the number of edges or cubically in the number of nodes. Mutually exclusive with other parameters. Default is None.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the Adamic-Adar computation: only Adamic-Adar indices for node pairs where the first node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. Can only be used together with the from_ parameter. When provided with from_, constrains the domain of the Adamic-Adar computation: only Adamic-Adar indices for node pairs where the first node is in from_ and the second node is in to are computed and returned. Default is None.
  • between

    (Relationship, default: None) - A binary relationship containing pairs of nodes. When provided, constrains the domain of the Adamic-Adar computation: only Adamic-Adar indices for the specific node pairs in this relationship are computed and returned. Mutually exclusive with other parameters. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and their Adamic-Adar index.

Raises:

  • DirectedGraphNotSupported - If the graph is directed.
  • TypeError - If from_, to, or between is not a Relationship.
  • ValueError - If full is provided with any other parameter. If between is provided with any other parameter. If from_ is provided with any parameter other than to. If to is provided without the from_ parameter. If none of full, from_, or between is provided. If full is not True or None. If from_, to, or between is not attached to the same model as the graph. If from_, to, or between does not contain the graph’s Node concept. If from_ or to is not a unary relationship. If between is not a binary relationship.
  • WeightedGraphNotSupported - If the graph is weighted.

Relationship Schema:

adamic_adar(node_u, node_v, score)

  • node_u (Node): The first node in the pair.
  • node_v (Node): The second node in the pair.
  • score (Float): The Adamic-Adar index of the pair.

Examples:

>>> from relationalai.semantics import Model, define, select, where, Float
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Select the Adamic-Adar indices from the full relationship
>>> u, v = Node.ref("u"), Node.ref("v")
>>> score = Float.ref("score")
>>> adamic_adar = graph.adamic_adar(full=True)
>>> model.select(
... u.id, v.id, score,
... ).where(
... adamic_adar(u, v, score),
... u.id == 2,
... v.id == 4,
... ).inspect()
id id_2 score
0 2 4 0.910239
>>> # 4. Use 'from_' parameter to constrain the set of nodes for the first position
>>> # Define a subset containing only node 1
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 1).define(subset(node))
>>>
>>> # Get Adamic-Adar indices only for pairs where first node is in subset
>>> constrained_adamic_adar = graph.adamic_adar(from_=subset)
>>> model.select(u.id, v.id, score).where(constrained_adamic_adar(u, v, score)).inspect()
id id_2 score
0 1 1 0.910239
1 1 3 0.910239
2 1 4 0.910239
>>> # 5. Use both 'from_' and 'to' parameters to constrain both positions
>>> subset_a = model.Relationship(f"{Node:node} is in subset_a")
>>> subset_b = model.Relationship(f"{Node:node} is in subset_b")
>>> model.where(node.id == 1).define(subset_a(node))
>>> model.where(node.id == 4).define(subset_b(node))
>>>
>>> # Get Adamic-Adar indices only where first node is in subset_a and second node is in subset_b
>>> constrained_adamic_adar = graph.adamic_adar(from_=subset_a, to=subset_b)
>>> model.select(u.id, v.id, score).where(constrained_adamic_adar(u, v, score)).inspect()
id id_2 score
0 1 4 0.910239
>>> # 6. Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 1, node_b.id == 4).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 2, node_b.id == 3).define(pairs(node_a, node_b))
>>>
>>> # Get Adamic-Adar indices only for the specific pairs (1, 4) and (2, 3)
>>> constrained_adamic_adar = graph.adamic_adar(between=pairs)
>>> model.select(u.id, v.id, score).where(constrained_adamic_adar(u, v, score)).inspect()
id id_2 score
0 1 4 0.910239
1 2 3 2.352934

Notes:

The Adamic-Adar index for nodes u and v is defined as the sum of the inverse logarithmic degree of their common neighbors w::

AA(u,v) = Σ (1 / log(degree(w)))

The adamic_adar(full=True) method computes and caches the full Adamic-Adar relationship for all pairs of nodes, providing efficient reuse across multiple calls. This can be expensive as dependencies can contain O(|E|²) or O(|V|³) tuples depending on graph density.

Calling adamic_adar() without arguments raises a ValueError, to ensure awareness and explicit acknowledgement (full=True) of this cost.

In contrast, adamic_adar(from_=subset) constrains the computation to tuples with the first position in the passed-in subset. The result is not cached; it is specific to the call site. When a significant fraction of the Adamic-Adar relation is needed across a program, adamic_adar(full=True) is typically more efficient. Use adamic_adar(from_=subset) only when small subsets of the Adamic-Adar relationship are needed collectively across the program.

The to parameter can be used together with from_ to further constrain the computation: adamic_adar(from_=subset_a, to=subset_b) computes Adamic-Adar indices only for node pairs where the first node is in subset_a and the second node is in subset_b. (Since adamic_adar is symmetric in its first two positions, using to without from_ would be functionally redundant, and is not allowed.)

The between parameter provides another way to constrain the computation. Unlike from_ and to, which allow you to independently constrain the first and second positions in adamic_adar tuples to sets of nodes, between allows you constrain the first and second positions, jointly, to specific pairs of nodes.

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.preferential_attachment(
*,
full: Optional[bool] = None,
from_: Optional[Relationship] = None,
to: Optional[Relationship] = None,
between: Optional[Relationship] = None
) -> Relationship
Undirected ✔Directed ✔Weighted ✘

Returns a ternary relationship containing the preferential attachment score for pairs of nodes.

The preferential attachment score between two nodes u and v is the product of the outdegree of u and the indegree of v for directed graphs, and the product of the degrees of u and v for undirected graphs.

Parameters:

  • full

    (bool, default: None) - If True, computes the preferential attachment score for all pairs of nodes in the graph. This computation can be expensive for large graphs, as the result can scale quadratically in the number of nodes. Mutually exclusive with other parameters. Default is None.
  • from_

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the preferential attachment computation: only preferential attachment scores for node pairs where the first node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • to

    (Relationship, default: None) - A unary relationship containing a subset of the graph’s nodes. When provided, constrains the domain of the preferential attachment computation: only preferential attachment scores for node pairs where the second node is in this relationship are computed and returned. Mutually exclusive with full and between. Default is None.
  • between

    (Relationship, default: None) - A binary relationship containing pairs of nodes. When provided, constrains the domain of the preferential attachment computation: only preferential attachment scores for the specific node pairs in this relationship are computed and returned. Mutually exclusive with other parameters. Default is None.

Returns:

  • Relationship - A ternary relationship where each tuple represents a pair of nodes and their preferential attachment score.

Raises:

  • TypeError - If from_, to, or between is not a Relationship.
  • ValueError - If full is provided with any other parameter. If between is provided with any other parameter. If from_ is provided with any parameter other than to. If to is provided with any parameter other than from_. If none of full, from_, to, or between is provided. If full is not True or None. If from_, to, or between is not attached to the same model as the graph. If from_, to, or between does not contain the graph’s Node concept. If from_ or to is not a unary relationship. If between is not a binary relationship.
  • WeightedGraphNotSupported - If the graph is weighted.

Relationship Schema:

preferential_attachment(node_u, node_v, score)

  • node_u (Node): The first node in the pair.
  • node_v (Node): The second node in the pair.
  • score (Integer): The preferential attachment score of the pair.

Examples:

Undirected Graph Example

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Select the preferential attachment scores from the full relationship
>>> u, v = Node.ref("u"), Node.ref("v")
>>> score = Integer.ref("score")
>>> preferential_attachment = graph.preferential_attachment(full=True)
>>> model.select(
... u.id, v.id, score,
... ).where(
... preferential_attachment(u, v, score),
... u.id == 1,
... model.union(v.id == 2, v.id == 3),
... ).inspect()
id id_2 score
0 1 2 3
1 1 3 4
>>> # 4. Use 'from_' parameter to constrain the set of nodes for the first position
>>> # Define a subset containing only node 1
>>> from relationalai.semantics import where
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 1).define(subset(node))
>>>
>>> # Get preferential attachment scores only for pairs where first node is in subset
>>> constrained_preferential_attachment = graph.preferential_attachment(from_=subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 1 1
1 1 2 3
2 1 3 4
3 1 4 2
>>> # 5. Use 'to' parameter to constrain the set of nodes for the second position
>>> # Get preferential attachment scores only for pairs where second node is in subset
>>> constrained_preferential_attachment = graph.preferential_attachment(to=subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 1 1
1 2 1 3
2 3 1 4
3 4 1 2
>>> # 6. Use both 'from_' and 'to' parameters to constrain both positions
>>> from_subset = model.Relationship(f"{Node:node} is in from_subset")
>>> to_subset = model.Relationship(f"{Node:node} is in to_subset")
>>> model.where(node.id == 1).define(from_subset(node))
>>> model.where(node.id == 3).define(to_subset(node))
>>>
>>> # Get preferential attachment scores only where first node is in from_subset and second node is in to_subset
>>> constrained_preferential_attachment = graph.preferential_attachment(from_=from_subset, to=to_subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 3 4
>>> # 7. Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 1, node_b.id == 3).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 2, node_b.id == 4).define(pairs(node_a, node_b))
>>>
>>> # Get preferential attachment scores only for the specific pairs (1, 3) and (2, 4)
>>> constrained_preferential_attachment = graph.preferential_attachment(between=pairs)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 3 4
1 2 4 6

Directed Graph Example

>>> from relationalai.semantics import Model, Integer, union
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up a directed graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=True, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4 = [Node.new(id=i) for i in range(1, 5)]
>>> model.define(n1, n2, n3, n4)
>>> model.define(
... Edge.new(src=n1, dst=n2),
... Edge.new(src=n2, dst=n3),
... Edge.new(src=n3, dst=n3),
... Edge.new(src=n2, dst=n4),
... Edge.new(src=n4, dst=n3),
... )
>>>
>>> # 3. Select the preferential attachment scores from the full relationship
>>> u, v = Node.ref("u"), Node.ref("v")
>>> score = Integer.ref("score")
>>> preferential_attachment = graph.preferential_attachment(full=True)
>>> model.select(
... u.id, v.id, score,
... ).where(
... preferential_attachment(u, v, score),
... u.id == 1,
... model.union(v.id == 2, v.id == 3),
... ).inspect()
id id_2 score
0 1 2 1
1 1 3 3
>>> # 4. Use 'from_' parameter to constrain the set of nodes for the first position
>>> # Define a subset containing only node 1
>>> subset = model.Relationship(f"{Node:node} is in subset")
>>> node = Node.ref()
>>> model.where(node.id == 1).define(subset(node))
>>>
>>> # Get preferential attachment scores only for pairs where first node is in subset
>>> constrained_preferential_attachment = graph.preferential_attachment(from_=subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 1 0
1 1 2 1
2 1 3 3
3 1 4 1
>>> # 5. Use 'to' parameter to constrain the set of nodes for the second position
>>> # Get preferential attachment scores only for pairs where second node is in subset
>>> constrained_preferential_attachment = graph.preferential_attachment(to=subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 1 0
1 2 1 0
2 3 1 0
3 4 1 0
>>> # 6. Use both 'from_' and 'to' parameters to constrain both positions
>>> from_subset = model.Relationship(f"{Node:node} is in from_subset")
>>> to_subset = model.Relationship(f"{Node:node} is in to_subset")
>>> model.where(node.id == 1).define(from_subset(node))
>>> model.where(node.id == 3).define(to_subset(node))
>>>
>>> # Get preferential attachment scores only where first node is in from_subset and second node is in to_subset
>>> constrained_preferential_attachment = graph.preferential_attachment(from_=from_subset, to=to_subset)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 3 3
>>> # 7. Use 'between' parameter to constrain to specific pairs of nodes
>>> pairs = model.Relationship(f"{Node:node_a} and {Node:node_b} are a pair")
>>> node_a, node_b = Node.ref(), Node.ref()
>>> model.where(node_a.id == 1, node_b.id == 3).define(pairs(node_a, node_b))
>>> model.where(node_a.id == 2, node_b.id == 4).define(pairs(node_a, node_b))
>>>
>>> # Get preferential attachment scores only for the specific pairs (1, 3) and (2, 4)
>>> constrained_preferential_attachment = graph.preferential_attachment(between=pairs)
>>> model.select(u.id, v.id, score).where(constrained_preferential_attachment(u, v, score)).inspect()
id id_2 score
0 1 3 3
1 2 4 2

Notes:

The preferential_attachment(full=True) method computes and caches the full preferential attachment relationship for all pairs of nodes, providing efficient reuse across multiple calls. This can be expensive as the result contains O(|V|²) tuples.

Calling preferential_attachment() without arguments raises a ValueError, to ensure awareness and explicit acknowledgement (full=True) of this cost.

In contrast, parameters from_, to, and between allow for constraining the computation to specific subsets of the graph. The result is not cached; it is specific to the call site. When a significant fraction of the preferential attachment relation is needed across a program, preferential_attachment(full=True) is typically more efficient. Use parameters from_, to, and between only when small subsets of the preferential attachment relationship are needed collectively across the program.

preferential_attachment(from_=subset) constrains the computation to tuples with the first position in the passed-in subset. preferential_attachment(to=subset) constrains the computation to tuples with the second position in the passed-in subset. In directed graphs, the preferential_attachment relationship is asymmetric: the computation considers out-edges from node_u and in-edges to node_v. This asymmetry means that from_ and to parameters have distinct, non-interchangeable meanings.

The to parameter can be used together with from_ to further constrain the computation: preferential_attachment(from_=subset_a, to=subset_b) computes preferential attachment scores only for node pairs where the first node is in subset_a and the second node is in subset_b.

The between parameter provides another way to constrain the computation. Unlike from_ and to, which allow you to independently constrain the first and second positions in preferential_attachment tuples to sets of nodes, between allows you constrain the first and second positions, jointly, to specific pairs of nodes.

Referenced By:

RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview
Graph.infomap(
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 = 8675309,
*,
diagnostic_info: bool = False
) -> Union[Relationship, Tuple[Relationship, Relationship]]
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Partitions nodes into communities using a variant of the Infomap algorithm.

This method maps nodes to community assignments based on the flow of information on the graph.

Parameters:

  • max_levels

    (int, default: 1) - The maximum number of levels at which to optimize. Must be a positive integer. Default is 1.
  • max_sweeps

    (int, default: 20) - The maximum number of sweeps within each level. Must be a non-negative integer. Default is 20.
  • level_tolerance

    (float, default: 0.01) - Map equation progress threshold to continue to the next level. Must be a non-negative float. Default is 0.01.
  • sweep_tolerance

    (float, default: 0.0001) - Map equation progress threshold to continue to the next sweep. Must be a non-negative float. Default is 0.0001.
  • teleportation_rate

    (float, default: 0.15) - Teleportation rate for ergodic node visit rate calculation. Must be a float in [1e-4, 1). Default is 0.15.
  • visit_rate_tolerance

    (float, default: 1e-15) - Convergence tolerance for ergodic node visit rate calculation. Must be a positive float. Default is 1e-15.
  • randomization_seed

    (int, default: 8675309) - The random number generator seed for the run. Must be a non-negative integer. Default is 8675309.
  • diagnostic_info

    (bool, default: False) - If True, returns diagnostic information alongside community assignments. If False (default), returns only community assignments.

Returns:

  • Relationship or tuple of Relationships - If diagnostic_info is False (default), returns a binary relationship where each tuple represents a node and its community assignment.

    If diagnostic_info is True, returns a tuple of (community_assignments, diagnostic_info) where:

    • community_assignments is the binary relationship described above
    • diagnostic_info is a binary relationship containing one tuple, whose second component is a diagnostic string describing the algorithm’s convergence and termination behavior

Relationship Schema:

When diagnostic_info=False (default):

infomap(node, community_label)

  • node (Node): The node.
  • community_label (Integer): The label of the community the node belongs to.

When diagnostic_info=True, returns two relationships:

  • infomap(node, community_label) as described above; and
  • diagnostic_info(diagnostic_name, diagnostic_value)
  • diagnostic_name (String): The name of the diagnostic information.
  • diagnostic_value (String): A diagnostic string describing the algorithm’s convergence and termination behavior.

Examples:

Unweighted Graph Example

Compute community assignments for each node in an undirected graph. Here, an undirected dumbbell graph resolves into two communities, namely its two constituent three-cliques.

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges for a dumbbell graph
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # The first three-clique.
... Edge.new(src=n1, dst=n2), Edge.new(src=n1, dst=n3), Edge.new(src=n2, dst=n3),
... # The second three-clique.
... Edge.new(src=n4, dst=n5), Edge.new(src=n4, dst=n6), Edge.new(src=n5, dst=n6),
... # The connection between the three-cliques.
... Edge.new(src=n1, dst=n4)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> infomap = graph.infomap()
>>> model.select(node.id, label).where(infomap(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Weighted Graph Example

Compute community assignments for each node in an undirected weighted graph. Here, a six-clique has the edges forming a dumbbell graph within the six-clique strongly weighted, and the remaining edges weakly weighted. The graph resolves into two communities, namely the two three-cliques constituent of the dumbbell embedded in the six-clique.

>>> # 1. Set up a weighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # First embedded three-clique.
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n1, dst=n3, weight=1.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... # Second embedded three-clique.
... Edge.new(src=n4, dst=n5, weight=1.0),
... Edge.new(src=n4, dst=n6, weight=1.0),
... Edge.new(src=n5, dst=n6, weight=1.0),
... # Slightly weaker connection between the embedded three-cliques.
... Edge.new(src=n1, dst=n4, weight=0.5),
... # Weaker edges connecting the six-clique in full.
... Edge.new(src=n1, dst=n5, weight=0.1), Edge.new(src=n1, dst=n6, weight=0.1),
... Edge.new(src=n2, dst=n4, weight=0.1), Edge.new(src=n2, dst=n5, weight=0.1),
... Edge.new(src=n2, dst=n6, weight=0.1), Edge.new(src=n3, dst=n4, weight=0.1),
... Edge.new(src=n3, dst=n5, weight=0.1), Edge.new(src=n3, dst=n6, weight=0.1)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> infomap = graph.infomap()
>>> model.select(node.id, label).where(infomap(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Notes:

This implementation of Infomap minimizes the map equation via a Louvain-like optimization heuristic; this is often referred to as “core” Infomap in the literature. Computation of the ergodic node visit frequencies is done via regularized power iteration, with regularization via a uniform teleportation probability of 0.15, matching the nominal selection in the literature.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.louvain(
max_levels: int = 1,
max_sweeps: int = 20,
level_tolerance: float = 0.01,
sweep_tolerance: float = 0.0001,
randomization_seed: int = 8675309,
*,
diagnostic_info: bool = False
) -> Union[Relationship, Tuple[Relationship, Relationship]]
Undirected ✔Directed ✘Weighted ✔Unweighted ✔

Partitions nodes into communities using the Louvain algorithm.

This method detects communities by maximizing a modularity score. It is only applicable to undirected graphs.

Parameters:

  • max_levels

    (int, default: 1) - The maximum number of levels at which to optimize. Must be a positive integer. Default is 1.
  • max_sweeps

    (int, default: 20) - The maximum number of sweeps within each level. Must be a non-negative integer. Default is 20.
  • level_tolerance

    (float, default: 0.01) - Modularity progress threshold to continue to the next level. Must be a non-negative float. Default is 0.01.
  • sweep_tolerance

    (float, default: 0.0001) - Modularity progress threshold to continue to the next sweep. Must be a non-negative float. Default is 0.0001.
  • randomization_seed

    (int, default: 8675309) - The random number generator seed for the run. Must be a non-negative integer. Default is 8675309.
  • diagnostic_info

    (bool, default: False) - If True, returns diagnostic information alongside community assignments. If False (default), returns only community assignments.

Returns:

  • Relationship or tuple of Relationships - If diagnostic_info is False (default), returns a binary relationship where each tuple represents a node and its community assignment.

    If diagnostic_info is True, returns a tuple of (community_assignments, diagnostic_info) where:

    • community_assignments is the binary relationship described above
    • diagnostic_info is a binary relationship containing one tuple, whose second component is a diagnostic string describing the algorithm’s convergence and termination behavior

Raises:

  • DirectedGraphNotSupported - If the graph is directed.

Relationship Schema:

When diagnostic_info=False (default):

louvain(node, community_label)

  • node (Node): The node.
  • community_label (Integer): The label of the community the node belongs to.

When diagnostic_info=True, returns two relationships:

  • louvain(node, community_label) as described above; and
  • diagnostic_info(diagnostic_name, diagnostic_value)
  • diagnostic_name (String): The name of the diagnostic information.
  • diagnostic_value (String): A diagnostic string describing the algorithm’s convergence and termination behavior.

Examples:

Unweighted Graph Example

Compute community assignments for each node in an undirected graph. Here, an undirected dumbbell graph resolves into two communities, namely its two constituent three-cliques.

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges for a dumbbell graph
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # The first three-clique.
... Edge.new(src=n1, dst=n2), Edge.new(src=n1, dst=n3), Edge.new(src=n2, dst=n3),
... # The second three-clique.
... Edge.new(src=n4, dst=n5), Edge.new(src=n4, dst=n6), Edge.new(src=n5, dst=n6),
... # The connection between the three-cliques.
... Edge.new(src=n1, dst=n4)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> louvain = graph.louvain()
>>> model.select(node.id, label).where(louvain(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Weighted Graph Example

Compute community assignments for each node in an undirected weighted graph. Here, a six-clique has the edges forming a dumbbell graph within the six-clique strongly weighted, and the remaining edges weakly weighted. The graph resolves into two communities, namely the two three-cliques constituent of the dumbbell embedded in the six-clique.

>>> # 1. Set up a weighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # First embedded three-clique.
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n1, dst=n3, weight=1.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... # Second embedded three-clique.
... Edge.new(src=n4, dst=n5, weight=1.0),
... Edge.new(src=n4, dst=n6, weight=1.0),
... Edge.new(src=n5, dst=n6, weight=1.0),
... # Connection between the embedded three-cliques.
... Edge.new(src=n1, dst=n4, weight=1.0),
... # Weaker edges connecting the six-clique in full.
... Edge.new(src=n1, dst=n5, weight=0.2), Edge.new(src=n1, dst=n6, weight=0.2),
... Edge.new(src=n2, dst=n4, weight=0.2), Edge.new(src=n2, dst=n5, weight=0.2),
... Edge.new(src=n2, dst=n6, weight=0.2), Edge.new(src=n3, dst=n4, weight=0.2),
... Edge.new(src=n3, dst=n5, weight=0.2), Edge.new(src=n3, dst=n6, weight=0.2)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> louvain = graph.louvain()
>>> model.select(node.id, label).where(louvain(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Notes:

This implementation of the Louvain algorithm is consistent with the modularity definition (Eq. 1) in “Fast unfolding of communities in large networks”, Blondel et al J. Stat. Mech. (2008) P10008.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
Graph.label_propagation(
max_sweeps: int = 20,
randomization_seed: int = 8675309,
*,
diagnostic_info: bool = False
) -> Union[Relationship, Tuple[Relationship, Relationship]]
Undirected ✔Directed ✔Weighted ✔Unweighted ✔

Partitions nodes into communities using the Label Propagation algorithm.

This method maps nodes to community assignments via asynchronous label propagation.

Parameters:

  • max_sweeps

    (int, default: 20) - The maximum number of sweeps for label propagation to perform. Must be a non-negative integer. Default is 20.
  • randomization_seed

    (int, default: 8675309) - The random number generator seed for the run. Must be a non-negative integer. Default is 8675309.
  • diagnostic_info

    (bool, default: False) - If True, returns diagnostic information alongside community assignments. If False (default), returns only community assignments.

Returns:

  • Relationship or tuple of Relationships - If diagnostic_info is False (default), returns a binary relationship where each tuple represents a node and its community assignment.

    If diagnostic_info is True, returns a tuple of (community_assignments, diagnostic_info) where:

    • community_assignments is the binary relationship described above
    • diagnostic_info is a binary relationship containing one tuple, whose second component is a diagnostic string describing the algorithm’s convergence and termination behavior

Relationship Schema:

When diagnostic_info=False (default):

label_propagation(node, community_label)

  • node (Node): The node.
  • community_label (Integer): The label of the community the node belongs to.

When diagnostic_info=True, returns two relationships:

  • label_propagation(node, community_label) as described above; and
  • diagnostic_info(diagnostic_name, diagnostic_value)
  • diagnostic_name (String): The name of the diagnostic information.
  • diagnostic_value (String): A diagnostic string describing the algorithm’s convergence and termination behavior.

Examples:

Unweighted Graph Example

Compute community assignments for each node in an undirected graph. Here, an undirected dumbbell graph resolves into two communities, namely its two constituent three-cliques.

>>> from relationalai.semantics import Model, Integer
>>> from relationalai.semantics.reasoners.graph import Graph
>>>
>>> # 1. Set up an undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=False)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges for a dumbbell graph
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # The first three-clique.
... Edge.new(src=n1, dst=n2), Edge.new(src=n1, dst=n3), Edge.new(src=n2, dst=n3),
... # The second three-clique.
... Edge.new(src=n4, dst=n5), Edge.new(src=n4, dst=n6), Edge.new(src=n5, dst=n6),
... # The connection between the three-cliques.
... Edge.new(src=n1, dst=n4)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> label_propagation = graph.label_propagation()
>>> model.select(node.id, label).where(label_propagation(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Weighted Graph Example

Compute community assignments for each node in an undirected weighted graph. Here, a six-clique has the edges forming a dumbbell graph within the six-clique strongly weighted, and the remaining edges weakly weighted. The graph resolves into two communities, namely the two three-cliques constituent of the dumbbell embedded in the six-clique.

>>> # 1. Set up a weighted, undirected graph
>>> model = Model("test_model")
>>> graph = Graph(model, directed=False, weighted=True)
>>> Node, Edge = graph.Node, graph.Edge
>>>
>>> # 2. Define nodes and edges
>>> n1, n2, n3, n4, n5, n6 = [Node.new(id=i) for i in range(1, 7)]
>>> model.define(n1, n2, n3, n4, n5, n6)
>>> model.define(
... # First embedded three-clique.
... Edge.new(src=n1, dst=n2, weight=1.0),
... Edge.new(src=n1, dst=n3, weight=1.0),
... Edge.new(src=n2, dst=n3, weight=1.0),
... # Second embedded three-clique.
... Edge.new(src=n4, dst=n5, weight=1.0),
... Edge.new(src=n4, dst=n6, weight=1.0),
... Edge.new(src=n5, dst=n6, weight=1.0),
... # Slightly weaker connection between the embedded three-cliques.
... Edge.new(src=n1, dst=n4, weight=0.5),
... # Weaker edges connecting the six-clique in full.
... Edge.new(src=n1, dst=n5, weight=0.1), Edge.new(src=n1, dst=n6, weight=0.1),
... Edge.new(src=n2, dst=n4, weight=0.1), Edge.new(src=n2, dst=n5, weight=0.1),
... Edge.new(src=n2, dst=n6, weight=0.1), Edge.new(src=n3, dst=n4, weight=0.1),
... Edge.new(src=n3, dst=n5, weight=0.1), Edge.new(src=n3, dst=n6, weight=0.1)
... )
>>>
>>> # 3. Compute community assignments and inspect
>>> node, label = Node.ref("node"), Integer.ref("label")
>>> label_propagation = graph.label_propagation()
>>> model.select(node.id, label).where(label_propagation(node, label)).inspect()
id label
0 1 2
1 2 2
2 3 2
3 4 1
4 5 1
5 6 1

Notes:

This implementation of asynchronous label propagation breaks ties between neighboring labels with equal cumulative edge weight (and frequency in the unweighted case) uniformly at random, but with a static seed.

Edges with zero weight still influence the result, for example if a node has only zero-weight edges.

Referenced By:

RelationalAI Documentation
└──  Build With RelationalAI
    └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
        └──  Run a Graph Algorithm
            └──  Available Algorithms
RelationalAI Documentation
├──  Build With RelationalAI
│   └──  Understand how PyRel works > Use advanced reasoning > Graph reasoning
│       ├──  Create a graph
│       │   ├──  Understand graph basics
│       │   └──  Choose a graph type
│       └──  Run a Graph Algorithm
│           └──  Available Algorithms
└──  Release Notes
    └──  Preview Features
        └──  Features In Preview