relationalai 0.12.4__py3-none-any.whl → 0.12.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- relationalai/__init__.py +4 -0
- relationalai/clients/snowflake.py +23 -11
- relationalai/{semantics/reasoners/graph → experimental}/paths/README.md +2 -2
- relationalai/experimental/paths/__init__.py +14 -309
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/basic_example.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_benchmark.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/paths_example.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/examples/pattern_to_automaton.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_repetition.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/single.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_repetition.py +1 -1
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/two_sided_balls_upto.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-old.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp-tuple.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/usp.py +3 -3
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_max_length.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_sp_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_limit_walks_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_repetition_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_one_sided_ball_upto_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_paths.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_single_walks_undirected.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_repetition_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_two_sided_balls_upto_single.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_multiple.py +2 -2
- relationalai/{semantics/reasoners/graph → experimental}/paths/tests/tests_usp_nsp_single.py +2 -2
- relationalai/semantics/__init__.py +4 -0
- relationalai/semantics/internal/annotations.py +1 -0
- relationalai/semantics/internal/internal.py +2 -0
- relationalai/semantics/lqp/builtins.py +1 -0
- relationalai/semantics/lqp/model2lqp.py +96 -3
- relationalai/semantics/lqp/primitives.py +3 -0
- relationalai/semantics/metamodel/builtins.py +50 -1
- relationalai/semantics/metamodel/typer/typer.py +3 -0
- relationalai/semantics/reasoners/__init__.py +4 -0
- relationalai/semantics/reasoners/experimental/__init__.py +7 -0
- relationalai/semantics/reasoners/graph/core.py +1154 -122
- relationalai/semantics/rel/builtins.py +3 -1
- relationalai/semantics/rel/rel_utils.py +5 -0
- relationalai/semantics/sql/compiler.py +6 -0
- {relationalai-0.12.4.dist-info → relationalai-0.12.6.dist-info}/METADATA +1 -1
- {relationalai-0.12.4.dist-info → relationalai-0.12.6.dist-info}/RECORD +84 -100
- {relationalai-0.12.4.dist-info → relationalai-0.12.6.dist-info}/WHEEL +1 -1
- relationalai/early_access/paths/__init__.py +0 -22
- relationalai/early_access/paths/api/__init__.py +0 -12
- relationalai/early_access/paths/benchmarks/__init__.py +0 -13
- relationalai/early_access/paths/graph/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/find_paths/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/one_sided_ball_repetition/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/one_sided_ball_upto/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/single/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/two_sided_balls_repetition/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/two_sided_balls_upto/__init__.py +0 -12
- relationalai/early_access/paths/path_algorithms/usp/__init__.py +0 -12
- relationalai/early_access/paths/rpq/__init__.py +0 -13
- relationalai/early_access/paths/utilities/iterators/__init__.py +0 -12
- relationalai/experimental/paths/pathfinder.rel +0 -2560
- relationalai/semantics/reasoners/graph/paths/__init__.py +0 -16
- relationalai/semantics/reasoners/graph/paths/path_algorithms/__init__.py +0 -3
- relationalai/semantics/reasoners/graph/paths/utilities/__init__.py +0 -3
- /relationalai/{semantics/reasoners/graph → experimental}/paths/api.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/benchmarks/grid_graph.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/code_organization.md +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/Movies.ipynb +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/minimal_engine_warmup.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movie_example.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/actedin.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/directed.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/follows.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/movies.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/person.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/produced.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/ratings.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/examples/movies_data/wrote.csv +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/find_paths_via_automaton.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/graph.py +0 -0
- /relationalai/{early_access → experimental}/paths/path_algorithms/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/find_paths.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/path_algorithms/one_sided_ball_upto.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/product_graph.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/automaton.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/diagnostics.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/filter.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/glushkov.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/rpq.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/rpq/transition.py +0 -0
- /relationalai/{early_access → experimental}/paths/utilities/__init__.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/iterators.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/prefix_sum.py +0 -0
- /relationalai/{semantics/reasoners/graph → experimental}/paths/utilities/utilities.py +0 -0
- {relationalai-0.12.4.dist-info → relationalai-0.12.6.dist-info}/entry_points.txt +0 -0
- {relationalai-0.12.4.dist-info → relationalai-0.12.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -21,7 +21,9 @@ from relationalai.semantics import (
|
|
|
21
21
|
)
|
|
22
22
|
from relationalai.docutils import include_in_docs
|
|
23
23
|
from relationalai.semantics.internal import annotations
|
|
24
|
+
from relationalai.semantics.internal import internal as builder_internal # For primitive graph algorithms.
|
|
24
25
|
from relationalai.semantics.std.math import abs, isnan, isinf, maximum, natural_log, sqrt
|
|
26
|
+
from relationalai.semantics.std.integers import int64
|
|
25
27
|
|
|
26
28
|
Numeric = Union[int, float, Decimal]
|
|
27
29
|
NumericType = Type[Union[Numeric, Number]]
|
|
@@ -1067,6 +1069,7 @@ class Graph():
|
|
|
1067
1069
|
def _validate_domain_constraint_parameters(
|
|
1068
1070
|
self,
|
|
1069
1071
|
method_name: str,
|
|
1072
|
+
symmetric: bool,
|
|
1070
1073
|
full: Optional[bool],
|
|
1071
1074
|
from_: Optional[Relationship],
|
|
1072
1075
|
to: Optional[Relationship],
|
|
@@ -1076,13 +1079,15 @@ class Graph():
|
|
|
1076
1079
|
Validate the domain constraint parameters for methods that accept
|
|
1077
1080
|
`full`, `from_`, `to`, and `between` parameters.
|
|
1078
1081
|
|
|
1079
|
-
This helper method performs common validation logic that applies
|
|
1080
|
-
across multiple graph methods (e.g., common_neighbor, adamic_adar).
|
|
1081
|
-
|
|
1082
1082
|
Parameters
|
|
1083
1083
|
----------
|
|
1084
1084
|
method_name : str
|
|
1085
1085
|
The name of the method being validated (for error messages).
|
|
1086
|
+
symmetric : bool
|
|
1087
|
+
Whether the relationship is symmetric in its first two positions.
|
|
1088
|
+
If True, enforces that 'to' can only be used with 'from_' (since
|
|
1089
|
+
'to' alone would be redundant for symmetric relations).
|
|
1090
|
+
If False, allows 'to' without 'from_' (for asymmetric relations).
|
|
1086
1091
|
full : bool, optional
|
|
1087
1092
|
The full parameter value.
|
|
1088
1093
|
from_ : Relationship, optional
|
|
@@ -1127,8 +1132,9 @@ class Graph():
|
|
|
1127
1132
|
"or use 'from_'/'to' to constrain by position."
|
|
1128
1133
|
)
|
|
1129
1134
|
|
|
1130
|
-
# Confirm that 'to' is only used with 'from_'.
|
|
1131
|
-
|
|
1135
|
+
# Confirm that 'to' is only used with 'from_' for symmetric relations.
|
|
1136
|
+
# For asymmetric relations, 'to' without 'from_' is meaningful.
|
|
1137
|
+
if symmetric and to is not None and from_ is None:
|
|
1132
1138
|
raise ValueError(
|
|
1133
1139
|
"The 'to' parameter can only be used together with the 'from_' parameter. "
|
|
1134
1140
|
f"The 'from_' parameter constrains the first position in {method_name} tuples, "
|
|
@@ -1142,6 +1148,7 @@ class Graph():
|
|
|
1142
1148
|
if (
|
|
1143
1149
|
full is None and
|
|
1144
1150
|
from_ is None and
|
|
1151
|
+
to is None and
|
|
1145
1152
|
between is None
|
|
1146
1153
|
):
|
|
1147
1154
|
raise ValueError(
|
|
@@ -2171,9 +2178,10 @@ class Graph():
|
|
|
2171
2178
|
1 2 4 3
|
|
2172
2179
|
|
|
2173
2180
|
"""
|
|
2174
|
-
# Validate domain constraint parameters.
|
|
2181
|
+
# Validate domain constraint parameters (common_neighbor is symmetric).
|
|
2182
|
+
symmetric = True
|
|
2175
2183
|
self._validate_domain_constraint_parameters(
|
|
2176
|
-
'common_neighbor', full, from_, to, between
|
|
2184
|
+
'common_neighbor', symmetric, full, from_, to, between
|
|
2177
2185
|
)
|
|
2178
2186
|
|
|
2179
2187
|
# At this point, exactly one of `full`, `from_`, or `between`
|
|
@@ -4941,6 +4949,11 @@ class Graph():
|
|
|
4941
4949
|
def reachable_from(self):
|
|
4942
4950
|
"""Returns a binary relationship of all pairs of nodes (u, v) where v is reachable from u.
|
|
4943
4951
|
|
|
4952
|
+
.. deprecated::
|
|
4953
|
+
The ``reachable_from`` method is deprecated and will be removed in a
|
|
4954
|
+
future release. Please use :meth:`reachable` instead, which provides
|
|
4955
|
+
the same functionality with additional domain constraint options.
|
|
4956
|
+
|
|
4944
4957
|
A node `v` is considered reachable from a node `u` if there is a path
|
|
4945
4958
|
of edges from `u` to `v`.
|
|
4946
4959
|
|
|
@@ -5043,35 +5056,370 @@ class Graph():
|
|
|
5043
5056
|
7 3 2
|
|
5044
5057
|
8 3 3
|
|
5045
5058
|
|
|
5059
|
+
See Also
|
|
5060
|
+
--------
|
|
5061
|
+
reachable
|
|
5046
5062
|
|
|
5047
5063
|
"""
|
|
5048
5064
|
warnings.warn(
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
"`reachable_from`'s interface will soon need to change."
|
|
5053
|
-
),
|
|
5054
|
-
FutureWarning,
|
|
5065
|
+
"The 'reachable_from' method is deprecated and will be removed in a future release. "
|
|
5066
|
+
"Please use 'reachable' instead.",
|
|
5067
|
+
DeprecationWarning,
|
|
5055
5068
|
stacklevel=2
|
|
5056
5069
|
)
|
|
5057
|
-
return self.
|
|
5070
|
+
return self.reachable(full=True)
|
|
5071
|
+
|
|
5072
|
+
|
|
5073
|
+
@include_in_docs
|
|
5074
|
+
def reachable(
|
|
5075
|
+
self,
|
|
5076
|
+
*,
|
|
5077
|
+
full: Optional[bool] = None,
|
|
5078
|
+
from_: Optional[Relationship] = None,
|
|
5079
|
+
to: Optional[Relationship] = None,
|
|
5080
|
+
between: Optional[Relationship] = None,
|
|
5081
|
+
):
|
|
5082
|
+
"""Returns a binary relationship of pairs of nodes (u, v) where v is reachable from u.
|
|
5083
|
+
|
|
5084
|
+
A node `v` is considered reachable from a node `u` if there is a path
|
|
5085
|
+
of edges from `u` to `v`.
|
|
5086
|
+
|
|
5087
|
+
Parameters
|
|
5088
|
+
----------
|
|
5089
|
+
full : bool, optional
|
|
5090
|
+
If ``True``, computes reachability for all pairs of nodes in the graph.
|
|
5091
|
+
This computation can be expensive for large graphs. Must be set to
|
|
5092
|
+
``True`` to compute the full reachable relationship. Cannot be used
|
|
5093
|
+
with other parameters.
|
|
5094
|
+
from_ : Relationship, optional
|
|
5095
|
+
A unary relationship containing a subset of the graph's nodes. When
|
|
5096
|
+
provided, constrains the first position of the reachable
|
|
5097
|
+
computation: only reachability from nodes in this relationship are
|
|
5098
|
+
returned. Cannot be used with other parameters.
|
|
5099
|
+
to : Relationship, optional
|
|
5100
|
+
A unary relationship containing a subset of the graph's nodes. When
|
|
5101
|
+
provided, constrains the second position of the reachable
|
|
5102
|
+
computation: only reachability to nodes in this relationship are
|
|
5103
|
+
returned. Cannot be used with other parameters.
|
|
5104
|
+
between : Relationship, optional
|
|
5105
|
+
Not yet supported for reachable. If provided, raises an error. If you
|
|
5106
|
+
need this capability, please reach out.
|
|
5107
|
+
|
|
5108
|
+
Returns
|
|
5109
|
+
-------
|
|
5110
|
+
Relationship
|
|
5111
|
+
A binary relationship where each tuple represents a node and a
|
|
5112
|
+
node that is reachable from it.
|
|
5113
|
+
|
|
5114
|
+
Relationship Schema
|
|
5115
|
+
-------------------
|
|
5116
|
+
``reachable(from_node, to_node)``
|
|
5117
|
+
|
|
5118
|
+
* **from_node** (*Node*): The node from which the path originates.
|
|
5119
|
+
* **to_node** (*Node*): The node that is reachable from the first node.
|
|
5120
|
+
|
|
5121
|
+
Supported Graph Types
|
|
5122
|
+
---------------------
|
|
5123
|
+
| Graph Type | Supported | Notes |
|
|
5124
|
+
| :--------- | :-------- | :------------------- |
|
|
5125
|
+
| Undirected | Yes | |
|
|
5126
|
+
| Directed | Yes | |
|
|
5127
|
+
| Weighted | Yes | Weights are ignored. |
|
|
5128
|
+
|
|
5129
|
+
Notes
|
|
5130
|
+
-----
|
|
5131
|
+
The ``reachable(full=True)`` method computes and caches the full
|
|
5132
|
+
reachable relationship, providing efficient reuse across multiple calls. In
|
|
5133
|
+
contrast, ``reachable(from_=subset)`` or ``reachable(to=subset)``
|
|
5134
|
+
compute constrained relationships specific to the passed-in subset and call
|
|
5135
|
+
site. When a significant fraction of the reachable relation is needed,
|
|
5136
|
+
``reachable(full=True)`` is typically more efficient. Use constrained
|
|
5137
|
+
variants only when small subsets are needed.
|
|
5138
|
+
|
|
5139
|
+
In directed graphs, the ``reachable`` relationship is asymmetric: node B
|
|
5140
|
+
may be reachable from node A without node A being reachable from node B. This
|
|
5141
|
+
asymmetry means that ``from_`` and ``to`` parameters have distinct,
|
|
5142
|
+
non-interchangeable meanings.
|
|
5143
|
+
|
|
5144
|
+
**Important:** Simultaneous use of ``from_`` and ``to`` parameters is not
|
|
5145
|
+
yet supported. The ``between`` parameter is also not yet supported. If
|
|
5146
|
+
you need these capabilities, please reach out.
|
|
5147
|
+
|
|
5148
|
+
There is a slight difference between `transitive closure` and
|
|
5149
|
+
`reachable`. The transitive closure of a binary relation E is the
|
|
5150
|
+
smallest relation that contains E and is transitive. When E is the
|
|
5151
|
+
edge set of a graph, the transitive closure of E does not include
|
|
5152
|
+
(u, u) if u is isolated. `reachable` is a different binary
|
|
5153
|
+
relation in which any node u is always reachable from u. In
|
|
5154
|
+
particular, `transitive closure` is a more general concept than
|
|
5155
|
+
`reachable`.
|
|
5156
|
+
|
|
5157
|
+
Examples
|
|
5158
|
+
--------
|
|
5159
|
+
**Directed Graph Example**
|
|
5160
|
+
|
|
5161
|
+
>>> from relationalai.semantics import Model, define, select
|
|
5162
|
+
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
5163
|
+
>>>
|
|
5164
|
+
>>> # 1. Set up a directed graph
|
|
5165
|
+
>>> model = Model("test_model")
|
|
5166
|
+
>>> graph = Graph(model, directed=True, weighted=False)
|
|
5167
|
+
>>> Node, Edge = graph.Node, graph.Edge
|
|
5168
|
+
>>>
|
|
5169
|
+
>>> # 2. Define nodes and edges
|
|
5170
|
+
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
|
|
5171
|
+
>>> define(n1, n2, n3)
|
|
5172
|
+
>>> define(
|
|
5173
|
+
... Edge.new(src=n1, dst=n2),
|
|
5174
|
+
... Edge.new(src=n3, dst=n2),
|
|
5175
|
+
... )
|
|
5176
|
+
>>>
|
|
5177
|
+
>>> # 3. Select all reachable pairs and inspect
|
|
5178
|
+
>>> from_node, to_node = Node.ref("start"), Node.ref("end")
|
|
5179
|
+
>>> reachable = graph.reachable(full=True)
|
|
5180
|
+
>>> select(from_node.id, to_node.id).where(reachable(from_node, to_node)).inspect()
|
|
5181
|
+
▰▰▰▰ Setup complete
|
|
5182
|
+
id id2
|
|
5183
|
+
0 1 1
|
|
5184
|
+
1 1 2
|
|
5185
|
+
2 2 2
|
|
5186
|
+
3 3 2
|
|
5187
|
+
4 3 3
|
|
5188
|
+
|
|
5189
|
+
>>> # 4. Use 'from_' parameter to get reachability from specific nodes
|
|
5190
|
+
>>> # Define a subset containing nodes 1 and 3
|
|
5191
|
+
>>> from relationalai.semantics import union, where
|
|
5192
|
+
>>> subset = model.Relationship(f"{{node:{Node}}} is in from subset")
|
|
5193
|
+
>>> node = Node.ref()
|
|
5194
|
+
>>> where(union(node.id == 1, node.id == 3)).define(subset(node))
|
|
5195
|
+
>>>
|
|
5196
|
+
>>> # Get reachability from nodes in the subset to all other nodes
|
|
5197
|
+
>>> reachable_from = graph.reachable(from_=subset)
|
|
5198
|
+
>>> select(from_node.id, to_node.id).where(
|
|
5199
|
+
... reachable_from(from_node, to_node)
|
|
5200
|
+
... ).inspect()
|
|
5201
|
+
▰▰▰▰ Setup complete
|
|
5202
|
+
id id2
|
|
5203
|
+
0 1 1
|
|
5204
|
+
1 1 2
|
|
5205
|
+
2 3 2
|
|
5206
|
+
3 3 3
|
|
5207
|
+
|
|
5208
|
+
>>> # 5. Use 'to' parameter to get reachability to specific nodes
|
|
5209
|
+
>>> # Define a different subset containing node 2
|
|
5210
|
+
>>> to_subset = model.Relationship(f"{{node:{Node}}} is in to subset")
|
|
5211
|
+
>>> node = Node.ref()
|
|
5212
|
+
>>> where(node.id == 2).define(to_subset(node))
|
|
5213
|
+
>>>
|
|
5214
|
+
>>> # Get reachability from all nodes to node 2
|
|
5215
|
+
>>> reachable_to = graph.reachable(to=to_subset)
|
|
5216
|
+
>>> select(from_node.id, to_node.id).where(
|
|
5217
|
+
... reachable_to(from_node, to_node)
|
|
5218
|
+
... ).inspect()
|
|
5219
|
+
▰▰▰▰ Setup complete
|
|
5220
|
+
id id2
|
|
5221
|
+
0 1 2
|
|
5222
|
+
1 2 2
|
|
5223
|
+
2 3 2
|
|
5224
|
+
|
|
5225
|
+
|
|
5226
|
+
**Undirected Graph Example**
|
|
5227
|
+
|
|
5228
|
+
>>> from relationalai.semantics import Model, define, select
|
|
5229
|
+
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
5230
|
+
>>>
|
|
5231
|
+
>>> # 1. Set up an undirected graph
|
|
5232
|
+
>>> model = Model("test_model")
|
|
5233
|
+
>>> graph = Graph(model, directed=False, weighted=False)
|
|
5234
|
+
>>> Node, Edge = graph.Node, graph.Edge
|
|
5235
|
+
>>>
|
|
5236
|
+
>>> # 2. Define the same nodes and edges
|
|
5237
|
+
>>> n1, n2, n3 = [Node.new(id=i) for i in range(1, 4)]
|
|
5238
|
+
>>> define(n1, n2, n3)
|
|
5239
|
+
>>> define(
|
|
5240
|
+
... Edge.new(src=n1, dst=n2),
|
|
5241
|
+
... Edge.new(src=n3, dst=n2),
|
|
5242
|
+
... )
|
|
5243
|
+
>>>
|
|
5244
|
+
>>> # 3. Select all reachable pairs and inspect
|
|
5245
|
+
>>> from_node, to_node = Node.ref("start"), Node.ref("end")
|
|
5246
|
+
>>> reachable = graph.reachable(full=True)
|
|
5247
|
+
>>> select(from_node.id, to_node.id).where(reachable(from_node, to_node)).inspect()
|
|
5248
|
+
▰▰▰▰ Setup complete
|
|
5249
|
+
id id2
|
|
5250
|
+
0 1 1
|
|
5251
|
+
1 1 2
|
|
5252
|
+
2 1 3
|
|
5253
|
+
3 2 1
|
|
5254
|
+
4 2 2
|
|
5255
|
+
5 2 3
|
|
5256
|
+
6 3 1
|
|
5257
|
+
7 3 2
|
|
5258
|
+
8 3 3
|
|
5259
|
+
|
|
5260
|
+
"""
|
|
5261
|
+
# Validate domain constraint parameters (reachable is asymmetric).
|
|
5262
|
+
symmetric = False
|
|
5263
|
+
self._validate_domain_constraint_parameters(
|
|
5264
|
+
'reachable', symmetric, full, from_, to, between
|
|
5265
|
+
)
|
|
5266
|
+
|
|
5267
|
+
# Reachable-specific validation: between is not yet supported.
|
|
5268
|
+
if between is not None:
|
|
5269
|
+
raise ValueError(
|
|
5270
|
+
"The 'between' parameter is not yet supported for reachable. "
|
|
5271
|
+
"Use 'full=True' for all-pairs reachability, or 'from_'/'to' to "
|
|
5272
|
+
"constrain by position. If you need 'between' support for reachable, "
|
|
5273
|
+
"please reach out."
|
|
5274
|
+
)
|
|
5275
|
+
|
|
5276
|
+
# Reachable-specific validation: from_+to combination is not yet supported.
|
|
5277
|
+
if from_ is not None and to is not None:
|
|
5278
|
+
raise ValueError(
|
|
5279
|
+
"Simultaneous use of 'from_' and 'to' is not yet supported for reachable. "
|
|
5280
|
+
"Use 'from_=subset' to constrain start nodes, 'to=subset' to constrain "
|
|
5281
|
+
"end nodes, or 'full=True' for all pairs. If you need the 'from_' and 'to' "
|
|
5282
|
+
"combination for reachable, please reach out."
|
|
5283
|
+
)
|
|
5284
|
+
|
|
5285
|
+
# At this point, exactly one of `full`, `from_`, or `to` has been provided.
|
|
5286
|
+
|
|
5287
|
+
if full is not None:
|
|
5288
|
+
return self._reachable
|
|
5289
|
+
|
|
5290
|
+
if from_ is not None:
|
|
5291
|
+
self._validate_node_subset_parameter('from_', from_)
|
|
5292
|
+
return self._reachable_from(from_)
|
|
5293
|
+
|
|
5294
|
+
if to is not None:
|
|
5295
|
+
self._validate_node_subset_parameter('to', to)
|
|
5296
|
+
return self._reachable_to(to)
|
|
5058
5297
|
|
|
5059
5298
|
@cached_property
|
|
5060
|
-
def
|
|
5061
|
-
"""Lazily define and cache the self.
|
|
5062
|
-
|
|
5063
|
-
|
|
5299
|
+
def _reachable(self):
|
|
5300
|
+
"""Lazily define and cache the self._reachable relationship."""
|
|
5301
|
+
_reachable_rel = self._create_reachable_relationship()
|
|
5302
|
+
_reachable_rel.annotate(annotations.track("graphs", "reachable"))
|
|
5303
|
+
return _reachable_rel
|
|
5304
|
+
|
|
5305
|
+
def _reachable_from(self, node_subset_from: Relationship):
|
|
5306
|
+
"""
|
|
5307
|
+
Create a reachable relationship with the first position constrained to
|
|
5308
|
+
nodes in `node_subset_from`. This computes reachability from nodes in
|
|
5309
|
+
the subset to all other nodes. Note this relationship
|
|
5310
|
+
is not cached; it is specific to the callsite.
|
|
5311
|
+
"""
|
|
5312
|
+
_reachable_rel = self._create_reachable_relationship(
|
|
5313
|
+
node_subset_from=node_subset_from,
|
|
5314
|
+
)
|
|
5315
|
+
_reachable_rel.annotate(annotations.track("graphs", "reachable_from"))
|
|
5316
|
+
return _reachable_rel
|
|
5064
5317
|
|
|
5318
|
+
def _reachable_to(self, node_subset_to: Relationship):
|
|
5319
|
+
"""
|
|
5320
|
+
Create a reachable relationship with the second position constrained to
|
|
5321
|
+
nodes in `node_subset_to`. This computes reachability from all nodes to
|
|
5322
|
+
nodes in the subset. Note this relationship
|
|
5323
|
+
is not cached; it is specific to the callsite.
|
|
5324
|
+
"""
|
|
5325
|
+
_reachable_rel = self._create_reachable_relationship(
|
|
5326
|
+
node_subset_to=node_subset_to
|
|
5327
|
+
)
|
|
5328
|
+
_reachable_rel.annotate(annotations.track("graphs", "reachable_to"))
|
|
5329
|
+
return _reachable_rel
|
|
5330
|
+
|
|
5331
|
+
def _create_reachable_relationship(
|
|
5332
|
+
self,
|
|
5333
|
+
*,
|
|
5334
|
+
node_subset_from: Optional[Relationship] = None,
|
|
5335
|
+
node_subset_to: Optional[Relationship] = None,
|
|
5336
|
+
):
|
|
5337
|
+
"""
|
|
5338
|
+
Create a reachable relationship, optionally constrained to node subsets.
|
|
5339
|
+
|
|
5340
|
+
Parameters
|
|
5341
|
+
----------
|
|
5342
|
+
node_subset_from : Relationship or None
|
|
5343
|
+
If provided, constrains the first position to this subset.
|
|
5344
|
+
node_subset_to : Relationship or None
|
|
5345
|
+
If provided, constrains the second position to this subset.
|
|
5346
|
+
|
|
5347
|
+
Returns
|
|
5348
|
+
-------
|
|
5349
|
+
Relationship
|
|
5350
|
+
A binary relationship mapping (from_node, to_node).
|
|
5351
|
+
"""
|
|
5352
|
+
# NOTE: In the constrained cases, we must compute over the full reach
|
|
5353
|
+
# {from xor to}, depending on the constraint, the nodes in the provided subset,
|
|
5354
|
+
# and _only_ over that reach.
|
|
5355
|
+
|
|
5356
|
+
# A reach relation over the reach {from xor to} the nodes in the subset.
|
|
5357
|
+
_reachable_rel = self._model.Relationship(
|
|
5358
|
+
f"{{node_a:{self._NodeConceptStr}}} reaches {{node_b:{self._NodeConceptStr}}}"
|
|
5359
|
+
)
|
|
5360
|
+
|
|
5361
|
+
# The logic below computes the reach by repeatedly extending
|
|
5362
|
+
# known reachability to neighbors; this snippet drives that,
|
|
5363
|
+
# with propagation direction depending on the constraint mode.
|
|
5065
5364
|
node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
|
|
5066
|
-
|
|
5067
|
-
|
|
5365
|
+
if node_subset_to is None:
|
|
5366
|
+
# Either of `full` or `from_` modes; propagate reach forward for both.
|
|
5367
|
+
# (`full` mode uses forward propagation as it may be slightly more efficient,
|
|
5368
|
+
# though backward propagation also works in principle.)
|
|
5369
|
+
extension_rule = where(
|
|
5370
|
+
_reachable_rel(node_a, node_b),
|
|
5371
|
+
self._edge(node_b, node_c),
|
|
5372
|
+
).select(node_a, node_c)
|
|
5373
|
+
else:
|
|
5374
|
+
# `to` mode; propagate reach backward.
|
|
5375
|
+
extension_rule = where(
|
|
5376
|
+
_reachable_rel(node_b, node_c),
|
|
5377
|
+
self._edge(node_a, node_b),
|
|
5378
|
+
).select(node_a, node_c)
|
|
5379
|
+
# NOTE: The optimizer may generate an index on _edge for the above;
|
|
5380
|
+
# it may be best to use a cached reversed edge relationship instead,
|
|
5381
|
+
# given it's useful elsewhere and that may yield reuse (and more reliably
|
|
5382
|
+
# good query plans, possibly).
|
|
5383
|
+
|
|
5384
|
+
# The set of nodes from which to propagate reach.
|
|
5385
|
+
node = self.Node.ref() # node is used (coupled) below.
|
|
5386
|
+
# Binding origin_nodes_constraint unconditionally for
|
|
5387
|
+
# the unconstrained case and then overriding if necessary
|
|
5388
|
+
# appeases the type checker, which otherwise thinks
|
|
5389
|
+
# it may be unbound below.
|
|
5390
|
+
origin_nodes_constraint = node
|
|
5391
|
+
if node_subset_from is not None:
|
|
5392
|
+
origin_nodes_constraint = node_subset_from(node)
|
|
5393
|
+
elif node_subset_to is not None:
|
|
5394
|
+
origin_nodes_constraint = node_subset_to(node)
|
|
5395
|
+
|
|
5396
|
+
# Generate union of reach implications between node_x and node_y:
|
|
5397
|
+
node_x, node_y = union(
|
|
5398
|
+
# A node is always reachable from itself.
|
|
5399
|
+
where(origin_nodes_constraint).select(node, node),
|
|
5400
|
+
# Reachability can be extended to neighbors.
|
|
5401
|
+
# (Note that this part of the rule also drives the reach.)
|
|
5402
|
+
extension_rule,
|
|
5403
|
+
)
|
|
5404
|
+
# Define the reach relation recursively.
|
|
5405
|
+
define(
|
|
5406
|
+
_reachable_rel(node_x, node_y)
|
|
5407
|
+
)
|
|
5068
5408
|
|
|
5069
|
-
return
|
|
5409
|
+
return _reachable_rel
|
|
5070
5410
|
|
|
5071
5411
|
|
|
5072
5412
|
@include_in_docs
|
|
5073
|
-
def distance(
|
|
5074
|
-
|
|
5413
|
+
def distance(
|
|
5414
|
+
self,
|
|
5415
|
+
*,
|
|
5416
|
+
full: Optional[bool] = None,
|
|
5417
|
+
from_: Optional[Relationship] = None,
|
|
5418
|
+
to: Optional[Relationship] = None,
|
|
5419
|
+
between: Optional[Relationship] = None,
|
|
5420
|
+
):
|
|
5421
|
+
"""Returns a ternary relationship containing
|
|
5422
|
+
the shortest path length between pairs of nodes.
|
|
5075
5423
|
|
|
5076
5424
|
This method computes the shortest path length between all pairs of
|
|
5077
5425
|
reachable nodes. The calculation depends on whether the graph is
|
|
@@ -5082,6 +5430,27 @@ class Graph():
|
|
|
5082
5430
|
* For **weighted** graphs, the length is the sum of edge weights
|
|
5083
5431
|
along the shortest path. Edge weights are assumed to be non-negative.
|
|
5084
5432
|
|
|
5433
|
+
Parameters
|
|
5434
|
+
----------
|
|
5435
|
+
full : bool, optional
|
|
5436
|
+
If ``True``, computes distances for all pairs of nodes in the graph.
|
|
5437
|
+
This computation can be expensive for large graphs. Must be set to
|
|
5438
|
+
``True`` to compute the full distance relationship. Cannot be used
|
|
5439
|
+
with other parameters.
|
|
5440
|
+
from_ : Relationship, optional
|
|
5441
|
+
A unary relationship containing a subset of the graph's nodes. When
|
|
5442
|
+
provided, constrains the first position (start_node) of the distance
|
|
5443
|
+
computation: only distances from nodes in this relationship are
|
|
5444
|
+
returned. Cannot be used with other parameters.
|
|
5445
|
+
to : Relationship, optional
|
|
5446
|
+
A unary relationship containing a subset of the graph's nodes. When
|
|
5447
|
+
provided, constrains the second position (end_node) of the distance
|
|
5448
|
+
computation: only distances to nodes in this relationship are
|
|
5449
|
+
returned. Cannot be used with other parameters.
|
|
5450
|
+
between : Relationship, optional
|
|
5451
|
+
Not yet supported for distance. If provided, raises an error. If you
|
|
5452
|
+
need this capability, please reach out.
|
|
5453
|
+
|
|
5085
5454
|
Returns
|
|
5086
5455
|
-------
|
|
5087
5456
|
Relationship
|
|
@@ -5105,11 +5474,30 @@ class Graph():
|
|
|
5105
5474
|
| Directed | Yes | |
|
|
5106
5475
|
| Weighted | Yes | The calculation uses edge weights. |
|
|
5107
5476
|
|
|
5477
|
+
Notes
|
|
5478
|
+
-----
|
|
5479
|
+
The ``distance(full=True)`` method computes and caches the full distance
|
|
5480
|
+
relationship, providing efficient reuse across multiple calls. In contrast,
|
|
5481
|
+
``distance(from_=subset)`` or ``distance(to=subset)`` compute constrained
|
|
5482
|
+
relationships specific to the passed-in subset and call site. When a
|
|
5483
|
+
significant fraction of the distance relation is needed, ``distance(full=True)``
|
|
5484
|
+
is typically more efficient. Use constrained variants only when small
|
|
5485
|
+
subsets are needed.
|
|
5486
|
+
|
|
5487
|
+
In directed graphs, the ``distance`` relationship is asymmetric: the
|
|
5488
|
+
distance from node A to node B may differ from the distance from node B
|
|
5489
|
+
to node A. This asymmetry means that ``from_`` and ``to`` parameters
|
|
5490
|
+
have distinct, non-interchangeable meanings.
|
|
5491
|
+
|
|
5492
|
+
**Important:** Simultaneous use of ``from_`` and ``to`` parameters is not
|
|
5493
|
+
yet supported. The ``between`` parameter is also not yet supported. If
|
|
5494
|
+
you need these capabilities, please reach out.
|
|
5495
|
+
|
|
5108
5496
|
Examples
|
|
5109
5497
|
--------
|
|
5110
5498
|
**Unweighted Graph Example**
|
|
5111
5499
|
|
|
5112
|
-
>>> from relationalai.semantics import Model, define, select, Integer
|
|
5500
|
+
>>> from relationalai.semantics import Model, define, select, union, where, Integer
|
|
5113
5501
|
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
5114
5502
|
>>>
|
|
5115
5503
|
>>> # 1. Set up an unweighted, undirected graph
|
|
@@ -5130,8 +5518,8 @@ class Graph():
|
|
|
5130
5518
|
>>> # 3. Select the shortest path length between all pairs of nodes
|
|
5131
5519
|
>>> start, end = Node.ref("start"), Node.ref("end")
|
|
5132
5520
|
>>> length = Integer.ref("length")
|
|
5133
|
-
>>>
|
|
5134
|
-
>>> select(start.id, end.id, length).where(
|
|
5521
|
+
>>> dist = graph.distance(full=True)
|
|
5522
|
+
>>> select(start.id, end.id, length).where(dist(start, end, length)).inspect()
|
|
5135
5523
|
▰▰▰▰ Setup complete
|
|
5136
5524
|
id id2 length
|
|
5137
5525
|
0 1 1 0
|
|
@@ -5151,6 +5539,46 @@ class Graph():
|
|
|
5151
5539
|
14 4 3 2
|
|
5152
5540
|
15 4 4 0
|
|
5153
5541
|
|
|
5542
|
+
>>> # 4. Use 'from_' parameter to get distances from specific nodes
|
|
5543
|
+
>>> # Define a subset containing nodes 1 and 3
|
|
5544
|
+
>>> subset = model.Relationship(f"{{node:{Node}}} is in from subset")
|
|
5545
|
+
>>> node = Node.ref()
|
|
5546
|
+
>>> where(union(node.id == 1, node.id == 3)).define(subset(node))
|
|
5547
|
+
>>>
|
|
5548
|
+
>>> # Get distances from nodes in the subset to all other nodes
|
|
5549
|
+
>>> dist_from = graph.distance(from_=subset)
|
|
5550
|
+
>>> select(start.id, end.id, length).where(
|
|
5551
|
+
... dist_from(start, end, length)
|
|
5552
|
+
... ).inspect()
|
|
5553
|
+
▰▰▰▰ Setup complete
|
|
5554
|
+
id id2 length
|
|
5555
|
+
0 1 1 0
|
|
5556
|
+
1 1 2 1
|
|
5557
|
+
2 1 3 2
|
|
5558
|
+
3 1 4 2
|
|
5559
|
+
4 3 1 2
|
|
5560
|
+
5 3 2 1
|
|
5561
|
+
6 3 3 0
|
|
5562
|
+
7 3 4 2
|
|
5563
|
+
|
|
5564
|
+
>>> # 5. Use 'to' parameter to get distances to specific nodes
|
|
5565
|
+
>>> # Define a different subset containing node 4
|
|
5566
|
+
>>> to_subset = model.Relationship(f"{{node:{Node}}} is in to subset")
|
|
5567
|
+
>>> node = Node.ref()
|
|
5568
|
+
>>> where(node.id == 4).define(to_subset(node))
|
|
5569
|
+
>>>
|
|
5570
|
+
>>> # Get distances from all nodes to node 4
|
|
5571
|
+
>>> dist_to = graph.distance(to=to_subset)
|
|
5572
|
+
>>> select(start.id, end.id, length).where(
|
|
5573
|
+
... dist_to(start, end, length)
|
|
5574
|
+
... ).inspect()
|
|
5575
|
+
▰▰▰▰ Setup complete
|
|
5576
|
+
id id2 length
|
|
5577
|
+
0 1 4 2
|
|
5578
|
+
1 2 4 1
|
|
5579
|
+
2 3 4 2
|
|
5580
|
+
3 4 4 0
|
|
5581
|
+
|
|
5154
5582
|
|
|
5155
5583
|
**Weighted Graph Example**
|
|
5156
5584
|
|
|
@@ -5174,8 +5602,8 @@ class Graph():
|
|
|
5174
5602
|
>>> # 3. Select the shortest path length between all pairs of nodes
|
|
5175
5603
|
>>> start, end = Node.ref("start"), Node.ref("end")
|
|
5176
5604
|
>>> length = Float.ref("length")
|
|
5177
|
-
>>>
|
|
5178
|
-
>>> select(start.id, end.id, length).where(
|
|
5605
|
+
>>> dist = graph.distance(full=True)
|
|
5606
|
+
>>> select(start.id, end.id, length).where(dist(start, end, length)).inspect()
|
|
5179
5607
|
▰▰▰▰ Setup complete
|
|
5180
5608
|
id id2 length
|
|
5181
5609
|
0 1 1 0.0
|
|
@@ -5187,75 +5615,190 @@ class Graph():
|
|
|
5187
5615
|
6 3 3 0.0
|
|
5188
5616
|
|
|
5189
5617
|
"""
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
"`distance`'s interface will soon need to change."
|
|
5195
|
-
),
|
|
5196
|
-
FutureWarning,
|
|
5197
|
-
stacklevel=2
|
|
5618
|
+
# Validate domain constraint parameters (distance is asymmetric).
|
|
5619
|
+
symmetric = False
|
|
5620
|
+
self._validate_domain_constraint_parameters(
|
|
5621
|
+
'distance', symmetric, full, from_, to, between
|
|
5198
5622
|
)
|
|
5199
5623
|
|
|
5200
|
-
|
|
5624
|
+
# Distance-specific validation: between is not yet supported.
|
|
5625
|
+
if between is not None:
|
|
5626
|
+
raise ValueError(
|
|
5627
|
+
"The 'between' parameter is not yet supported for distance. "
|
|
5628
|
+
"Use 'full=True' for all-pairs distances, or 'from_'/'to' to "
|
|
5629
|
+
"constrain by position. If you need 'between' support for distance, "
|
|
5630
|
+
"please reach out."
|
|
5631
|
+
)
|
|
5632
|
+
|
|
5633
|
+
# Distance-specific validation: from_+to combination is not yet supported.
|
|
5634
|
+
if from_ is not None and to is not None:
|
|
5635
|
+
raise ValueError(
|
|
5636
|
+
"Simultaneous use of 'from_' and 'to' is not yet supported for distance. "
|
|
5637
|
+
"Use 'from_=subset' to constrain start nodes, 'to=subset' to constrain "
|
|
5638
|
+
"end nodes, or 'full=True' for all pairs. If you need the 'from_' and 'to' "
|
|
5639
|
+
"combination for distance, please reach out."
|
|
5640
|
+
)
|
|
5641
|
+
|
|
5642
|
+
# At this point, exactly one of `full`, `from_`, or `to` has been provided.
|
|
5643
|
+
|
|
5644
|
+
if full is not None:
|
|
5645
|
+
return self._distance
|
|
5646
|
+
|
|
5647
|
+
if from_ is not None:
|
|
5648
|
+
self._validate_node_subset_parameter('from_', from_)
|
|
5649
|
+
return self._distance_from(from_)
|
|
5650
|
+
|
|
5651
|
+
if to is not None:
|
|
5652
|
+
self._validate_node_subset_parameter('to', to)
|
|
5653
|
+
return self._distance_to(to)
|
|
5201
5654
|
|
|
5202
5655
|
@cached_property
|
|
5203
5656
|
def _distance(self):
|
|
5204
5657
|
"""Lazily define and cache the self._distance relationship."""
|
|
5205
|
-
|
|
5206
|
-
_distance_rel = self._distance_non_weighted
|
|
5207
|
-
else:
|
|
5208
|
-
_distance_rel = self._distance_weighted
|
|
5209
|
-
|
|
5658
|
+
_distance_rel = self._create_distance_relationship()
|
|
5210
5659
|
_distance_rel.annotate(annotations.track("graphs", "distance"))
|
|
5211
5660
|
return _distance_rel
|
|
5212
5661
|
|
|
5213
|
-
|
|
5214
|
-
|
|
5215
|
-
|
|
5216
|
-
|
|
5217
|
-
|
|
5218
|
-
|
|
5219
|
-
|
|
5220
|
-
|
|
5221
|
-
|
|
5222
|
-
.select(node_u, node_v, d2) # Recursive case.
|
|
5662
|
+
def _distance_from(self, node_subset_from: Relationship):
|
|
5663
|
+
"""
|
|
5664
|
+
Create a distance relationship with the first position constrained to
|
|
5665
|
+
nodes in `node_subset_from`. This computes distances from nodes in
|
|
5666
|
+
the subset to all other nodes. Note this relationship
|
|
5667
|
+
is not cached; it is specific to the callsite.
|
|
5668
|
+
"""
|
|
5669
|
+
_distance_rel = self._create_distance_relationship(
|
|
5670
|
+
node_subset_from=node_subset_from,
|
|
5223
5671
|
)
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
return _distance_weighted_rel
|
|
5672
|
+
_distance_rel.annotate(annotations.track("graphs", "distance_from"))
|
|
5673
|
+
return _distance_rel
|
|
5227
5674
|
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5675
|
+
def _distance_to(self, node_subset_to: Relationship):
|
|
5676
|
+
"""
|
|
5677
|
+
Create a distance relationship with the second position constrained to
|
|
5678
|
+
nodes in `node_subset_to`. This computes distances from all nodes to
|
|
5679
|
+
nodes in the subset. Note this relationship
|
|
5680
|
+
is not cached; it is specific to the callsite.
|
|
5681
|
+
"""
|
|
5682
|
+
_distance_rel = self._create_distance_relationship(
|
|
5683
|
+
node_subset_to=node_subset_to
|
|
5237
5684
|
)
|
|
5238
|
-
|
|
5685
|
+
_distance_rel.annotate(annotations.track("graphs", "distance_to"))
|
|
5686
|
+
return _distance_rel
|
|
5239
5687
|
|
|
5240
|
-
|
|
5688
|
+
def _create_distance_relationship(
|
|
5689
|
+
self,
|
|
5690
|
+
*,
|
|
5691
|
+
node_subset_from: Optional[Relationship] = None,
|
|
5692
|
+
node_subset_to: Optional[Relationship] = None,
|
|
5693
|
+
weighted: Optional[bool] = None,
|
|
5694
|
+
):
|
|
5695
|
+
"""
|
|
5696
|
+
Create a distance relationship, weighted or unweighted per the graph,
|
|
5697
|
+
and optionally constrained to node subsets.
|
|
5241
5698
|
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5699
|
+
Parameters
|
|
5700
|
+
----------
|
|
5701
|
+
node_subset_from : Relationship or None
|
|
5702
|
+
If provided, constrains the first position to this subset.
|
|
5703
|
+
node_subset_to : Relationship or None
|
|
5704
|
+
If provided, constrains the second position to this subset.
|
|
5705
|
+
|
|
5706
|
+
Returns
|
|
5707
|
+
-------
|
|
5708
|
+
Relationship
|
|
5709
|
+
A ternary relationship mapping (from_node, to_node, distance).
|
|
5710
|
+
"""
|
|
5711
|
+
# NOTE: In the constrained cases, we must compute over the full reach
|
|
5712
|
+
# from xor to (depending on the constraint) the nodes in the provided subset,
|
|
5713
|
+
# and _only_ over that reach. To do so, the logic below simultaneously
|
|
5714
|
+
# computes the appropriate reach of the nodes in the provided subset
|
|
5715
|
+
# (in `_distance_reach_rel`) while computing distances.
|
|
5716
|
+
|
|
5717
|
+
if weighted is None:
|
|
5718
|
+
weighted = self.weighted
|
|
5719
|
+
|
|
5720
|
+
dist_type = Float if weighted else Integer
|
|
5721
|
+
|
|
5722
|
+
# A distance relation over the appropriate reach of the nodes in the subset.
|
|
5723
|
+
_distance_reach_rel = self._model.Relationship(
|
|
5724
|
+
f"{{node_u:{self._NodeConceptStr}}} and {{node_v:{self._NodeConceptStr}}} have a distance of {{d:{dist_type}}}"
|
|
5251
5725
|
)
|
|
5252
|
-
define(_distance_reversed_non_weighted_rel(node_u, node_v, min(d).per(node_u, node_v)))
|
|
5253
5726
|
|
|
5254
|
-
|
|
5727
|
+
# The logic below computes the reach and distances by repeatedly extending
|
|
5728
|
+
# known distances to neighbors; this snippet drives that, with propagation
|
|
5729
|
+
# direction depending on the constraint mode.
|
|
5730
|
+
node_u, node_v, neighbor = self.Node.ref(), self.Node.ref(), self.Node.ref()
|
|
5731
|
+
uv_dist, step_dist, neighbor_dist = dist_type.ref(), dist_type.ref(), dist_type.ref()
|
|
5732
|
+
if node_subset_to is None:
|
|
5733
|
+
# Either of `full` or `from` modes; propagate reach forward for both.
|
|
5734
|
+
# (`full` mode uses forward propagation as it may be slightly more efficient,
|
|
5735
|
+
# though backward propagation also works in principle.)
|
|
5736
|
+
extension_rule = where(
|
|
5737
|
+
_distance_reach_rel(node_u, node_v, uv_dist),
|
|
5738
|
+
*(
|
|
5739
|
+
(
|
|
5740
|
+
self._weight(node_v, neighbor, step_dist),
|
|
5741
|
+
neighbor_dist == uv_dist + abs(step_dist),
|
|
5742
|
+
)
|
|
5743
|
+
if weighted else
|
|
5744
|
+
(
|
|
5745
|
+
self._edge(node_v, neighbor),
|
|
5746
|
+
neighbor_dist == uv_dist + 1,
|
|
5747
|
+
)
|
|
5748
|
+
),
|
|
5749
|
+
).select(node_u, neighbor, neighbor_dist)
|
|
5750
|
+
else:
|
|
5751
|
+
# `to` mode; propagate reach backward.
|
|
5752
|
+
extension_rule = where(
|
|
5753
|
+
_distance_reach_rel(node_u, node_v, uv_dist),
|
|
5754
|
+
*(
|
|
5755
|
+
(
|
|
5756
|
+
self._weight(neighbor, node_u, step_dist),
|
|
5757
|
+
neighbor_dist == uv_dist + abs(step_dist),
|
|
5758
|
+
)
|
|
5759
|
+
if weighted else
|
|
5760
|
+
(
|
|
5761
|
+
self._edge(neighbor, node_u),
|
|
5762
|
+
neighbor_dist == uv_dist + 1,
|
|
5763
|
+
)
|
|
5764
|
+
),
|
|
5765
|
+
).select(neighbor, node_v, neighbor_dist)
|
|
5766
|
+
# NOTE: The optimizer may generate an index on _edge for the above;
|
|
5767
|
+
# it may be best to use a cached reversed edge/weight relationship instead,
|
|
5768
|
+
# given it's useful elsewhere and that may yield reuse (and more reliably
|
|
5769
|
+
# good query plans, possibly).
|
|
5770
|
+
|
|
5771
|
+
# The set of nodes from which to propagate reach and distances.
|
|
5772
|
+
node = self.Node.ref() # node is used (coupled) below.
|
|
5773
|
+
# Binding origin_nodes_constraint unconditionally for
|
|
5774
|
+
# the unconstrained case and then overriding if necessary
|
|
5775
|
+
# appeases the type checker, which otherwise thinks
|
|
5776
|
+
# it may be unbound below.
|
|
5777
|
+
origin_nodes_constraint = node
|
|
5778
|
+
if node_subset_from is not None:
|
|
5779
|
+
origin_nodes_constraint = node_subset_from(node)
|
|
5780
|
+
elif node_subset_to is not None:
|
|
5781
|
+
origin_nodes_constraint = node_subset_to(node)
|
|
5782
|
+
|
|
5783
|
+
# Generate union of possible distances between node_a and node_b:
|
|
5784
|
+
node_a, node_b, ab_dist = union(
|
|
5785
|
+
# The distance from a node to itself is zero.
|
|
5786
|
+
where(origin_nodes_constraint).select(node, node, 0),
|
|
5787
|
+
# The distance from one node to another can be extended to its neighbors.
|
|
5788
|
+
# (Note that this part of the rule also drives the reach.)
|
|
5789
|
+
extension_rule,
|
|
5790
|
+
)
|
|
5791
|
+
# From the union of possible distances between node_a and node_b,
|
|
5792
|
+
# select the minimum as the distance.
|
|
5793
|
+
define(
|
|
5794
|
+
_distance_reach_rel(node_a, node_b, min(ab_dist).per(node_a, node_b))
|
|
5795
|
+
)
|
|
5796
|
+
|
|
5797
|
+
return _distance_reach_rel
|
|
5255
5798
|
|
|
5256
5799
|
|
|
5257
5800
|
@include_in_docs
|
|
5258
|
-
def weakly_connected_component(self):
|
|
5801
|
+
def weakly_connected_component(self, *, of: Optional[Relationship] = None):
|
|
5259
5802
|
"""Returns a binary relationship that maps each node to its weakly connected component.
|
|
5260
5803
|
|
|
5261
5804
|
A weakly connected component is a subgraph where for every pair of
|
|
@@ -5263,6 +5806,14 @@ class Graph():
|
|
|
5263
5806
|
For undirected graphs, this is equivalent to finding the connected
|
|
5264
5807
|
components.
|
|
5265
5808
|
|
|
5809
|
+
Parameters
|
|
5810
|
+
----------
|
|
5811
|
+
of : Relationship, optional
|
|
5812
|
+
A unary relationship containing a subset of the graph's nodes. When
|
|
5813
|
+
provided, constrains the domain of the weakly connected component
|
|
5814
|
+
computation: only component memberships of nodes in this relationship
|
|
5815
|
+
are returned.
|
|
5816
|
+
|
|
5266
5817
|
Returns
|
|
5267
5818
|
-------
|
|
5268
5819
|
Relationship
|
|
@@ -5289,9 +5840,26 @@ class Graph():
|
|
|
5289
5840
|
The ``component_id`` is the node with the minimum ID within each
|
|
5290
5841
|
component.
|
|
5291
5842
|
|
|
5843
|
+
The ``weakly_connected_component()`` method, called with no parameters,
|
|
5844
|
+
computes and caches the full weakly connected component relationship,
|
|
5845
|
+
providing efficient reuse across multiple calls to
|
|
5846
|
+
``weakly_connected_component()``.
|
|
5847
|
+
|
|
5848
|
+
In contrast, ``weakly_connected_component(of=subset)`` computes a constrained
|
|
5849
|
+
relationship specific to the passed-in ``subset`` and that call site.
|
|
5850
|
+
|
|
5851
|
+
Note that the constrained computation requires working over all nodes
|
|
5852
|
+
in the components containing the nodes in ``subset``. When that set
|
|
5853
|
+
of nodes constitutes an appreciable fraction of the graph, the constrained
|
|
5854
|
+
computation may be less efficient than computing the full relationship.
|
|
5855
|
+
Use ``weakly_connected_component(of=subset)`` only when small subsets of
|
|
5856
|
+
the weakly connected component relationship are needed collectively
|
|
5857
|
+
across the program, and the associated components cover only a small
|
|
5858
|
+
part of the graph.
|
|
5859
|
+
|
|
5292
5860
|
Examples
|
|
5293
5861
|
--------
|
|
5294
|
-
>>> from relationalai.semantics import Model, define, select
|
|
5862
|
+
>>> from relationalai.semantics import Model, define, select, union, where
|
|
5295
5863
|
>>> from relationalai.semantics.reasoners.graph import Graph
|
|
5296
5864
|
>>>
|
|
5297
5865
|
>>> # 1. Set up a directed graph
|
|
@@ -5317,36 +5885,141 @@ class Graph():
|
|
|
5317
5885
|
1 2 1
|
|
5318
5886
|
2 3 1
|
|
5319
5887
|
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
|
|
5329
|
-
)
|
|
5888
|
+
>>> # 4. Use 'of' parameter to constrain computation to subset of nodes
|
|
5889
|
+
>>> # Define a subset containing only nodes 1 and 3
|
|
5890
|
+
>>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
|
|
5891
|
+
>>> node = Node.ref()
|
|
5892
|
+
>>> where(union(node.id == 1, node.id == 3)).define(subset(node))
|
|
5893
|
+
>>>
|
|
5894
|
+
>>> # Get component membership only for nodes in the subset
|
|
5895
|
+
>>> constrained_wcc = graph.weakly_connected_component(of=subset)
|
|
5896
|
+
>>> select(node.id, component_id.id).where(
|
|
5897
|
+
... constrained_wcc(node, component_id)
|
|
5898
|
+
... ).inspect()
|
|
5899
|
+
▰▰▰▰ Setup complete
|
|
5900
|
+
id id2
|
|
5901
|
+
0 1 1
|
|
5902
|
+
1 3 1
|
|
5330
5903
|
|
|
5331
|
-
|
|
5904
|
+
"""
|
|
5905
|
+
if of is None:
|
|
5906
|
+
return self._weakly_connected_component
|
|
5907
|
+
else:
|
|
5908
|
+
# Validate the 'of' parameter
|
|
5909
|
+
self._validate_node_subset_parameter('of', of)
|
|
5910
|
+
return self._weakly_connected_component_of(of)
|
|
5332
5911
|
|
|
5333
5912
|
@cached_property
|
|
5334
5913
|
def _weakly_connected_component(self):
|
|
5335
5914
|
"""Lazily define and cache the self._weakly_connected_component relationship."""
|
|
5336
|
-
_weakly_connected_component_rel = self.
|
|
5915
|
+
_weakly_connected_component_rel = self._create_weakly_connected_component_relationship(
|
|
5916
|
+
node_subset=None
|
|
5917
|
+
)
|
|
5337
5918
|
_weakly_connected_component_rel.annotate(annotations.track("graphs", "weakly_connected_component"))
|
|
5919
|
+
return _weakly_connected_component_rel
|
|
5338
5920
|
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
|
|
5343
|
-
|
|
5344
|
-
|
|
5921
|
+
def _weakly_connected_component_of(self, node_subset: Relationship):
|
|
5922
|
+
"""
|
|
5923
|
+
Create a weakly_connected_component relationship constrained to the
|
|
5924
|
+
subset of nodes in `node_subset`. Note this relationship is not cached;
|
|
5925
|
+
it is specific to the callsite.
|
|
5926
|
+
"""
|
|
5927
|
+
_weakly_connected_component_rel = self._create_weakly_connected_component_relationship(
|
|
5928
|
+
node_subset=node_subset
|
|
5345
5929
|
)
|
|
5346
|
-
|
|
5347
|
-
|
|
5930
|
+
_weakly_connected_component_rel.annotate(annotations.track("graphs", "weakly_connected_component_of"))
|
|
5348
5931
|
return _weakly_connected_component_rel
|
|
5349
5932
|
|
|
5933
|
+
def _create_weakly_connected_component_relationship(
|
|
5934
|
+
self, *, node_subset: Optional[Relationship]
|
|
5935
|
+
):
|
|
5936
|
+
"""
|
|
5937
|
+
Create a weakly_connected_component relationship, optionally constrained
|
|
5938
|
+
to a subset of nodes.
|
|
5939
|
+
|
|
5940
|
+
Parameters
|
|
5941
|
+
----------
|
|
5942
|
+
node_subset : Relationship or None
|
|
5943
|
+
If provided, a unary relationship defining the subset of nodes to
|
|
5944
|
+
compute component membership for. If None, compute for all nodes.
|
|
5945
|
+
|
|
5946
|
+
Returns
|
|
5947
|
+
-------
|
|
5948
|
+
Relationship
|
|
5949
|
+
A binary relationship mapping nodes to their weakly connected component IDs.
|
|
5950
|
+
"""
|
|
5951
|
+
# NOTE: In the constrained case, we must compute over the full of
|
|
5952
|
+
# the weakly connected components containing the nodes in the
|
|
5953
|
+
# provided subset, and _only_ over those weakly connected components.
|
|
5954
|
+
# To do so, the logic below simultaneously computes the (weak) reach from
|
|
5955
|
+
# the nodes in the provided subset (in `_weakly_connected_component_reach_rel``)
|
|
5956
|
+
# while computing component membership.
|
|
5957
|
+
|
|
5958
|
+
# A weakly connected component relation over
|
|
5959
|
+
# those components reachable from the nodes in the subset.
|
|
5960
|
+
_weakly_connected_component_reach_rel = self._model.Relationship(
|
|
5961
|
+
f"{{node:{self._NodeConceptStr}}} is in the connected component {{id:{self._NodeConceptStr}}}"
|
|
5962
|
+
)
|
|
5963
|
+
|
|
5964
|
+
node, neighbor, component, dummy = \
|
|
5965
|
+
self.Node.ref(), self.Node.ref(), self.Node.ref(), self.Node.ref()
|
|
5966
|
+
|
|
5967
|
+
# `discovered_nodes` reflects the (weak) reach from the nodes in the provided
|
|
5968
|
+
# subset known at a given point in the recursion. In the unconstrained
|
|
5969
|
+
# case, the subset is implicitly the set of all nodes, and starting with
|
|
5970
|
+
# all such nodes "discovered" accelerates the computation.
|
|
5971
|
+
if node_subset is None:
|
|
5972
|
+
discovered_nodes = node
|
|
5973
|
+
else:
|
|
5974
|
+
discovered_nodes = union(
|
|
5975
|
+
node_subset(node),
|
|
5976
|
+
_weakly_connected_component_reach_rel(node, dummy)
|
|
5977
|
+
)
|
|
5978
|
+
|
|
5979
|
+
where(
|
|
5980
|
+
# Generate union of possible component identifiers for a given node:
|
|
5981
|
+
union(
|
|
5982
|
+
# A node's component identifier may be itself.
|
|
5983
|
+
where(discovered_nodes, component == node),
|
|
5984
|
+
# A node's component identifier may be those of its neighbors.
|
|
5985
|
+
# (Note that this part of the rule also drives the (weak) reach.)
|
|
5986
|
+
where(
|
|
5987
|
+
self._neighbor(neighbor, node),
|
|
5988
|
+
_weakly_connected_component_reach_rel(neighbor, component)
|
|
5989
|
+
)
|
|
5990
|
+
)
|
|
5991
|
+
).define(
|
|
5992
|
+
# From the union of possible component identifiers for a given node,
|
|
5993
|
+
# select the minimum as the component identifier.
|
|
5994
|
+
_weakly_connected_component_reach_rel(node, min(component).per(node))
|
|
5995
|
+
)
|
|
5996
|
+
# NOTE: This logic, including in the constrained case, consumes
|
|
5997
|
+
# the (unconstrained, cached) self._neighbor relation, which is
|
|
5998
|
+
# ~O(nodes) to compute. Note that this seems to be about the best we can do:
|
|
5999
|
+
# To compute the (weak) reach, we either need: a) the edge relation, and
|
|
6000
|
+
# O(nodes) scans over the edge relation; b) the edge relation and
|
|
6001
|
+
# reverse edge relation, forming which is O(nodes); or c) the neighbor
|
|
6002
|
+
# relation over all nodes in the (weak) reach, computing which requires
|
|
6003
|
+
# one of (a) or (b) above. Given that, for simplicity here we use (c),
|
|
6004
|
+
# as there's a reasonable likelihood of re-/shared-use.
|
|
6005
|
+
|
|
6006
|
+
if node_subset is None:
|
|
6007
|
+
# The reach relation from all nodes is the full relation.
|
|
6008
|
+
return _weakly_connected_component_reach_rel
|
|
6009
|
+
else:
|
|
6010
|
+
# A weakly connected component relation constrained to
|
|
6011
|
+
# nodes in the subset (filtered from the reach relation above).
|
|
6012
|
+
_weakly_connected_component_constrained_rel = self._model.Relationship(
|
|
6013
|
+
f"{{node:{self._NodeConceptStr}}} is in the connected component {{id:{self._NodeConceptStr}}}"
|
|
6014
|
+
)
|
|
6015
|
+
|
|
6016
|
+
where(
|
|
6017
|
+
node_subset(node),
|
|
6018
|
+
_weakly_connected_component_reach_rel(node, component)
|
|
6019
|
+
).define(_weakly_connected_component_constrained_rel(node, component))
|
|
6020
|
+
|
|
6021
|
+
return _weakly_connected_component_constrained_rel
|
|
6022
|
+
|
|
5350
6023
|
|
|
5351
6024
|
@include_in_docs
|
|
5352
6025
|
def diameter_range(self):
|
|
@@ -5517,6 +6190,28 @@ class Graph():
|
|
|
5517
6190
|
|
|
5518
6191
|
return _reachable_from_min_node_rel
|
|
5519
6192
|
|
|
6193
|
+
@cached_property
|
|
6194
|
+
def _distance_non_weighted(self):
|
|
6195
|
+
"""Lazily define and cache the self._distance relationship."""
|
|
6196
|
+
if not self.weighted:
|
|
6197
|
+
return self._distance
|
|
6198
|
+
else:
|
|
6199
|
+
return self._create_distance_relationship(weighted=False)
|
|
6200
|
+
|
|
6201
|
+
@cached_property
|
|
6202
|
+
def _distance_reversed_non_weighted(self):
|
|
6203
|
+
"""Lazily define and cache the self._distance_reversed_non_weighted relationship, a non-public helper."""
|
|
6204
|
+
_distance_reversed_non_weighted_rel = self._model.Relationship(f"{{node_u:{self._NodeConceptStr}}} and {{node_v:{self._NodeConceptStr}}} have a reversed distance of {{d:Integer}}")
|
|
6205
|
+
node_u, node_v, node_n, d1 = self.Node.ref(), self.Node.ref(), self.Node.ref(), Integer.ref()
|
|
6206
|
+
node_u, node_v, d = union(
|
|
6207
|
+
where(node_u == node_v, d1 == 0).select(node_u, node_v, d1), # Base case.
|
|
6208
|
+
where(self._edge(node_v, node_n),
|
|
6209
|
+
d2 := _distance_reversed_non_weighted_rel(node_u, node_n, Integer) + 1).select(node_u, node_v, d2) # Recursive case.
|
|
6210
|
+
)
|
|
6211
|
+
define(_distance_reversed_non_weighted_rel(node_u, node_v, min(d).per(node_u, node_v)))
|
|
6212
|
+
|
|
6213
|
+
return _distance_reversed_non_weighted_rel
|
|
6214
|
+
|
|
5520
6215
|
|
|
5521
6216
|
@include_in_docs
|
|
5522
6217
|
def is_connected(self):
|
|
@@ -5901,9 +6596,10 @@ class Graph():
|
|
|
5901
6596
|
doi: 10.1162/netn_a_00199. PMID: 34746624; PMCID: PMC8567827.
|
|
5902
6597
|
|
|
5903
6598
|
"""
|
|
5904
|
-
# Validate domain constraint parameters.
|
|
6599
|
+
# Validate domain constraint parameters (jaccard_similarity is symmetric).
|
|
6600
|
+
symmetric = True
|
|
5905
6601
|
self._validate_domain_constraint_parameters(
|
|
5906
|
-
'jaccard_similarity', full, from_, to, between
|
|
6602
|
+
'jaccard_similarity', symmetric, full, from_, to, between
|
|
5907
6603
|
)
|
|
5908
6604
|
|
|
5909
6605
|
# At this point, exactly one of `full`, `from_`, or `between`
|
|
@@ -6476,9 +7172,10 @@ class Graph():
|
|
|
6476
7172
|
1 3 4 0.707107
|
|
6477
7173
|
|
|
6478
7174
|
"""
|
|
6479
|
-
# Validate domain constraint parameters.
|
|
7175
|
+
# Validate domain constraint parameters (cosine_similarity is symmetric).
|
|
7176
|
+
symmetric = True
|
|
6480
7177
|
self._validate_domain_constraint_parameters(
|
|
6481
|
-
'cosine_similarity', full, from_, to, between
|
|
7178
|
+
'cosine_similarity', symmetric, full, from_, to, between
|
|
6482
7179
|
)
|
|
6483
7180
|
|
|
6484
7181
|
# At this point, exactly one of `full`, `from_`, or `between`
|
|
@@ -6869,9 +7566,10 @@ class Graph():
|
|
|
6869
7566
|
1 2 3 1.442695
|
|
6870
7567
|
|
|
6871
7568
|
"""
|
|
6872
|
-
# Validate domain constraint parameters.
|
|
7569
|
+
# Validate domain constraint parameters (adamic_adar is symmetric).
|
|
7570
|
+
symmetric = True
|
|
6873
7571
|
self._validate_domain_constraint_parameters(
|
|
6874
|
-
'adamic_adar', full, from_, to, between
|
|
7572
|
+
'adamic_adar', symmetric, full, from_, to, between
|
|
6875
7573
|
)
|
|
6876
7574
|
|
|
6877
7575
|
# At this point, exactly one of `full`, `from_`, or `between`
|
|
@@ -7216,9 +7914,10 @@ class Graph():
|
|
|
7216
7914
|
1 2 4 6
|
|
7217
7915
|
|
|
7218
7916
|
"""
|
|
7219
|
-
# Validate domain constraint parameters.
|
|
7917
|
+
# Validate domain constraint parameters (preferential_attachment is symmetric).
|
|
7918
|
+
symmetric = True
|
|
7220
7919
|
self._validate_domain_constraint_parameters(
|
|
7221
|
-
'preferential_attachment', full, from_, to, between
|
|
7920
|
+
'preferential_attachment', symmetric, full, from_, to, between
|
|
7222
7921
|
)
|
|
7223
7922
|
|
|
7224
7923
|
# At this point, exactly one of `full`, `from_`, or `between`
|
|
@@ -7472,6 +8171,197 @@ class Graph():
|
|
|
7472
8171
|
return _isolated_node_rel
|
|
7473
8172
|
|
|
7474
8173
|
|
|
8174
|
+
@cached_property
|
|
8175
|
+
def _non_isolated_node(self):
|
|
8176
|
+
"""Lazily define and cache the self._non_isolated_node relationship."""
|
|
8177
|
+
# Primarily a helper for the primitive graph algorithms at this time.
|
|
8178
|
+
_non_isolated_node_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} is not isolated")
|
|
8179
|
+
|
|
8180
|
+
node, neighbor = self.Node.ref(), self.Node.ref()
|
|
8181
|
+
where(node, self._neighbor(node, neighbor)).define(_non_isolated_node_rel(node))
|
|
8182
|
+
|
|
8183
|
+
return _non_isolated_node_rel
|
|
8184
|
+
|
|
8185
|
+
|
|
8186
|
+
@cached_property
|
|
8187
|
+
def _primitive_node_to_index_rel(self):
|
|
8188
|
+
"""
|
|
8189
|
+
The graph primitives operate over node indices, contiguous from one;
|
|
8190
|
+
compute a map from the identifier for each non-isolated node to such an index.
|
|
8191
|
+
Lazily define and cache that relationship for shared use across primitive algorithms.
|
|
8192
|
+
"""
|
|
8193
|
+
_node_to_index_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has {{index:Integer}}")
|
|
8194
|
+
|
|
8195
|
+
node = self.Node.ref()
|
|
8196
|
+
where(
|
|
8197
|
+
self._non_isolated_node(node),
|
|
8198
|
+
index := rank(node)
|
|
8199
|
+
).define(
|
|
8200
|
+
_node_to_index_rel(node, index)
|
|
8201
|
+
)
|
|
8202
|
+
|
|
8203
|
+
return _node_to_index_rel
|
|
8204
|
+
|
|
8205
|
+
@cached_property
|
|
8206
|
+
def _primitive_weight_list(self):
|
|
8207
|
+
"""
|
|
8208
|
+
The graph primitives operate over a normalized weight list, where
|
|
8209
|
+
the nodes are represented by their contiguous indices as Int64s.
|
|
8210
|
+
Lazily define and cache that normalized weight list for shared use
|
|
8211
|
+
across primitive algorithms.
|
|
8212
|
+
"""
|
|
8213
|
+
_normalized_weight_list = self._model.Relationship("{u:Int64} {v:Int64} {w:Float}")
|
|
8214
|
+
|
|
8215
|
+
src_node, dst_node, weight = self.Node.ref(), self.Node.ref(), Float.ref()
|
|
8216
|
+
src_index, dst_index = Integer.ref(), Integer.ref()
|
|
8217
|
+
where(
|
|
8218
|
+
self._weight(src_node, dst_node, weight),
|
|
8219
|
+
self._primitive_node_to_index_rel(src_node, src_index),
|
|
8220
|
+
self._primitive_node_to_index_rel(dst_node, dst_index)
|
|
8221
|
+
).define(
|
|
8222
|
+
_normalized_weight_list(int64(src_index), int64(dst_index), weight)
|
|
8223
|
+
)
|
|
8224
|
+
|
|
8225
|
+
return _normalized_weight_list
|
|
8226
|
+
|
|
8227
|
+
@cached_property
|
|
8228
|
+
def _primitive_node_count(self):
|
|
8229
|
+
"""
|
|
8230
|
+
The graph primitives operate only over non-isolated nodes;
|
|
8231
|
+
compute the number of non-isolated nodes as an Int64.
|
|
8232
|
+
Lazily define and cache that count for shared use across primitive algorithms.
|
|
8233
|
+
"""
|
|
8234
|
+
_normalized_node_count = self._model.Relationship("{node_count:Int64}")
|
|
8235
|
+
|
|
8236
|
+
define(_normalized_node_count(
|
|
8237
|
+
int64(count(self.Node).where(self._non_isolated_node(self.Node)))
|
|
8238
|
+
))
|
|
8239
|
+
|
|
8240
|
+
return _normalized_node_count
|
|
8241
|
+
|
|
8242
|
+
@cached_property
|
|
8243
|
+
def _primitive_edge_count(self):
|
|
8244
|
+
"""
|
|
8245
|
+
The graph primitives operate over a normalized weight list;
|
|
8246
|
+
compute the number of normalized edges as an Int64.
|
|
8247
|
+
Lazily define and cache that count for shared use across primitive algorithms.
|
|
8248
|
+
|
|
8249
|
+
(Note that the count of normalized edges does not in general match
|
|
8250
|
+
graph.num_edges(); it should match for directed graphs, but not for
|
|
8251
|
+
undirected graphs, where num_edges computes the number of
|
|
8252
|
+
undirected rather than directed edges.)
|
|
8253
|
+
"""
|
|
8254
|
+
_normalized_edge_count = self._model.Relationship("{edge_count:Int64}")
|
|
8255
|
+
|
|
8256
|
+
u_index, v_index = builder_internal.Int64.ref(), builder_internal.Int64.ref()
|
|
8257
|
+
define(_normalized_edge_count(
|
|
8258
|
+
int64(count(u_index, v_index).where(self._primitive_weight_list(u_index, v_index, Float)))
|
|
8259
|
+
))
|
|
8260
|
+
|
|
8261
|
+
return _normalized_edge_count
|
|
8262
|
+
|
|
8263
|
+
def _create_primitive_algorithm_relationship(
|
|
8264
|
+
self,
|
|
8265
|
+
primitive_name: str,
|
|
8266
|
+
primitive_params: list,
|
|
8267
|
+
):
|
|
8268
|
+
"""
|
|
8269
|
+
Helper method for infomap, louvain, and label propagation,
|
|
8270
|
+
i.e. the graph algorithsm that exercise graph primitives.
|
|
8271
|
+
|
|
8272
|
+
Create community assignment and diagnostic information relationships
|
|
8273
|
+
exercising the specified primitive algorithm with the provided parameters.
|
|
8274
|
+
"""
|
|
8275
|
+
# Create relationship in which to store the final community assignments.
|
|
8276
|
+
_community_assignments_rel = self._model.Relationship(
|
|
8277
|
+
f"{{node:{self._NodeConceptStr}}} belongs to {{community:Integer}}")
|
|
8278
|
+
|
|
8279
|
+
# The graph primitives operate over a normalized form of the graph.
|
|
8280
|
+
# Most of the logic below transforms from the graph, to that normalized
|
|
8281
|
+
# form, and back again.
|
|
8282
|
+
|
|
8283
|
+
# The graph primitives operate over node indices, contiguous from one;
|
|
8284
|
+
# compute a map from the identifier for each non-isolated node to such an index.
|
|
8285
|
+
_primitive_node_to_index_rel = self._primitive_node_to_index_rel
|
|
8286
|
+
|
|
8287
|
+
# The graph primitives operate over a normalized weight list, where
|
|
8288
|
+
# the nodes are represented by their contiguous indices as Int64s.
|
|
8289
|
+
_primitive_weight_list = self._primitive_weight_list
|
|
8290
|
+
|
|
8291
|
+
# Compute the number of non-isolated nodes and normalized edges, as int64s.
|
|
8292
|
+
# (Note that the count of normalized edges does not in general match
|
|
8293
|
+
# graph.num_edges(); it should match for directed graphs, but not for
|
|
8294
|
+
# undirected graphs, where num_edges computes the number of
|
|
8295
|
+
# undirected rather than directed edges.)
|
|
8296
|
+
_primitive_node_count = self._primitive_node_count
|
|
8297
|
+
_primitive_edge_count = self._primitive_edge_count
|
|
8298
|
+
|
|
8299
|
+
# Invoke the graph primitive over the normalized data.
|
|
8300
|
+
primitive_expr = builder_internal.Expression(
|
|
8301
|
+
builder_internal.Relationship.builtins[primitive_name],
|
|
8302
|
+
builder_internal.TypeRef(_primitive_weight_list),
|
|
8303
|
+
builder_internal.TypeRef(_primitive_node_count),
|
|
8304
|
+
builder_internal.TypeRef(_primitive_edge_count),
|
|
8305
|
+
*primitive_params,
|
|
8306
|
+
builder_internal.String.ref("diagnostic_info"),
|
|
8307
|
+
builder_internal.Int64.ref("node_index"),
|
|
8308
|
+
builder_internal.Int64.ref("community")
|
|
8309
|
+
)
|
|
8310
|
+
|
|
8311
|
+
last_input_arg_offset = 3 + len(primitive_params) - 1
|
|
8312
|
+
prim_diagnostic_info = primitive_expr._arg_ref(last_input_arg_offset + 1)
|
|
8313
|
+
prim_node_index = primitive_expr._arg_ref(last_input_arg_offset + 2)
|
|
8314
|
+
prim_community = primitive_expr._arg_ref(last_input_arg_offset + 3)
|
|
8315
|
+
|
|
8316
|
+
# Extract the primitive's community assignments for
|
|
8317
|
+
# non-isolated nodes into a relation, mapping from
|
|
8318
|
+
# Int64s to Integers for smoother consumption downstream.
|
|
8319
|
+
_primitive_assignments_rel = self._model.Relationship("{node_index:Integer} has {community:Integer}")
|
|
8320
|
+
define(_primitive_assignments_rel(prim_node_index, prim_community))
|
|
8321
|
+
# TODO: May be possible to remove this intermediate relationship,
|
|
8322
|
+
# by directly mapping into `_infomap_result_rel` below.
|
|
8323
|
+
# But if so, need to take care not to introduce recursion in rules below.
|
|
8324
|
+
|
|
8325
|
+
# Transform the primitive's community assignments, which map
|
|
8326
|
+
# node indices to communities, to a map from nodes to communities.
|
|
8327
|
+
# Note that this covers only non-isolated nodes; isolated handled later.
|
|
8328
|
+
node = self.Node.ref()
|
|
8329
|
+
node_index = Integer.ref()
|
|
8330
|
+
community = Integer.ref()
|
|
8331
|
+
where(
|
|
8332
|
+
_primitive_node_to_index_rel(node, node_index),
|
|
8333
|
+
_primitive_assignments_rel(node_index, community),
|
|
8334
|
+
).define(
|
|
8335
|
+
_community_assignments_rel(node, community)
|
|
8336
|
+
)
|
|
8337
|
+
|
|
8338
|
+
# Each isolated node must be assigned to its own unique community.
|
|
8339
|
+
# The primitive's community assignments are contiguous integers from one,
|
|
8340
|
+
# so we can assign isolated nodes to communities by offsetting their
|
|
8341
|
+
# enumeration index by the maximum community assigned by the primitive.
|
|
8342
|
+
isolated_node = self.Node.ref()
|
|
8343
|
+
nonisolated_index = Integer.ref()
|
|
8344
|
+
nonisolated_comm = Integer.ref()
|
|
8345
|
+
where(
|
|
8346
|
+
self._isolated_node(isolated_node),
|
|
8347
|
+
isolated_node_rank := rank(isolated_node),
|
|
8348
|
+
max_nonisolated_comm := (
|
|
8349
|
+
max(nonisolated_index, nonisolated_comm).where(
|
|
8350
|
+
_primitive_assignments_rel(nonisolated_index, nonisolated_comm)
|
|
8351
|
+
) | 0
|
|
8352
|
+
),
|
|
8353
|
+
isolated_comm := isolated_node_rank + max_nonisolated_comm
|
|
8354
|
+
).define(_community_assignments_rel(isolated_node, isolated_comm))
|
|
8355
|
+
|
|
8356
|
+
# Extract diagnostic information from the primitive.
|
|
8357
|
+
_diagnostic_info_rel = self._model.Relationship("{diagnostic_info:String}")
|
|
8358
|
+
define(_diagnostic_info_rel(prim_diagnostic_info))
|
|
8359
|
+
|
|
8360
|
+
# Return both the community assignments and diagnostic information.
|
|
8361
|
+
return _community_assignments_rel, _diagnostic_info_rel
|
|
8362
|
+
|
|
8363
|
+
|
|
8364
|
+
@include_in_docs
|
|
7475
8365
|
def infomap(
|
|
7476
8366
|
self,
|
|
7477
8367
|
max_levels: int = 1,
|
|
@@ -7481,6 +8371,7 @@ class Graph():
|
|
|
7481
8371
|
teleportation_rate: float = 0.15,
|
|
7482
8372
|
visit_rate_tolerance: float = 1e-15,
|
|
7483
8373
|
randomization_seed: int = 8675309,
|
|
8374
|
+
diagnostic_info: bool = False,
|
|
7484
8375
|
):
|
|
7485
8376
|
"""Partitions nodes into communities using a variant of the Infomap algorithm.
|
|
7486
8377
|
|
|
@@ -7510,20 +8401,42 @@ class Graph():
|
|
|
7510
8401
|
randomization_seed : int, optional
|
|
7511
8402
|
The random number generator seed for the run. Must be a non-negative
|
|
7512
8403
|
integer. Default is 8675309.
|
|
8404
|
+
diagnostic_info : bool, optional
|
|
8405
|
+
If ``True``, returns diagnostic information alongside
|
|
8406
|
+
community assignments. If ``False`` (default), returns only community
|
|
8407
|
+
assignments.
|
|
7513
8408
|
|
|
7514
8409
|
Returns
|
|
7515
8410
|
-------
|
|
7516
|
-
Relationship
|
|
7517
|
-
|
|
7518
|
-
community
|
|
8411
|
+
Relationship or tuple of Relationships
|
|
8412
|
+
If ``diagnostic_info`` is ``False`` (default), returns a binary
|
|
8413
|
+
relationship where each tuple represents a node and its community
|
|
8414
|
+
assignment.
|
|
8415
|
+
|
|
8416
|
+
If ``diagnostic_info`` is ``True``, returns a tuple of
|
|
8417
|
+
``(community_assignments, diagnostic_info)`` where:
|
|
8418
|
+
|
|
8419
|
+
- ``community_assignments`` is the binary relationship described above
|
|
8420
|
+
- ``diagnostic_info`` is a unary relationship containing a diagnostic
|
|
8421
|
+
string describing the algorithm's convergence and termination behavior
|
|
7519
8422
|
|
|
7520
8423
|
Relationship Schema
|
|
7521
8424
|
-------------------
|
|
8425
|
+
When ``diagnostic_info=False`` (default):
|
|
8426
|
+
|
|
7522
8427
|
``infomap(node, community_label)``
|
|
7523
8428
|
|
|
7524
8429
|
* **node** (*Node*): The node.
|
|
7525
8430
|
* **community_label** (*Integer*): The label of the community the node
|
|
7526
|
-
|
|
8431
|
+
belongs to.
|
|
8432
|
+
|
|
8433
|
+
When ``diagnostic_info=True``, returns two relationships:
|
|
8434
|
+
|
|
8435
|
+
- ``infomap(node, community_label)`` as described above; and
|
|
8436
|
+
- ``diagnostic_info(diagnostic_string)``
|
|
8437
|
+
|
|
8438
|
+
* **diagnostic_string** (*String*): A diagnostic string describing the
|
|
8439
|
+
algorithm's convergence and termination behavior.
|
|
7527
8440
|
|
|
7528
8441
|
Supported Graph Types
|
|
7529
8442
|
---------------------
|
|
@@ -7650,8 +8563,37 @@ class Graph():
|
|
|
7650
8563
|
_assert_type("infomap:randomization_seed", randomization_seed, int)
|
|
7651
8564
|
_assert_exclusive_lower_bound("infomap:randomization_seed", randomization_seed, 0)
|
|
7652
8565
|
|
|
7653
|
-
|
|
8566
|
+
# Collect parameters to rel_primitive_infomap,
|
|
8567
|
+
# appropriately typed and ordered for the primitive.
|
|
8568
|
+
infomap_parameters = [
|
|
8569
|
+
float(teleportation_rate),
|
|
8570
|
+
float(visit_rate_tolerance),
|
|
8571
|
+
float(level_tolerance),
|
|
8572
|
+
float(sweep_tolerance),
|
|
8573
|
+
int(max_levels),
|
|
8574
|
+
int(max_sweeps),
|
|
8575
|
+
int(randomization_seed),
|
|
8576
|
+
]
|
|
8577
|
+
|
|
8578
|
+
# Create infomap community assignment and, if requested,
|
|
8579
|
+
# diagnostic information relationships.
|
|
8580
|
+
_infomap_assignments_rel, _infomap_diagnostic_rel = \
|
|
8581
|
+
self._create_primitive_algorithm_relationship(
|
|
8582
|
+
"infomap", infomap_parameters
|
|
8583
|
+
)
|
|
8584
|
+
|
|
8585
|
+
# Attach tracking information to the community assignment relationship.
|
|
8586
|
+
_infomap_assignments_rel.annotate(annotations.track("graphs", "infomap"))
|
|
8587
|
+
|
|
8588
|
+
# Return either just the community assignments, or both
|
|
8589
|
+
# the community assignments and diagnostic information, per request.
|
|
8590
|
+
if diagnostic_info:
|
|
8591
|
+
return _infomap_assignments_rel, _infomap_diagnostic_rel
|
|
8592
|
+
else:
|
|
8593
|
+
return _infomap_assignments_rel
|
|
8594
|
+
|
|
7654
8595
|
|
|
8596
|
+
@include_in_docs
|
|
7655
8597
|
def louvain(
|
|
7656
8598
|
self,
|
|
7657
8599
|
max_levels: int = 1,
|
|
@@ -7659,6 +8601,7 @@ class Graph():
|
|
|
7659
8601
|
level_tolerance: float = 0.01,
|
|
7660
8602
|
sweep_tolerance: float = 0.0001,
|
|
7661
8603
|
randomization_seed: int = 8675309,
|
|
8604
|
+
diagnostic_info: bool = False,
|
|
7662
8605
|
):
|
|
7663
8606
|
"""Partitions nodes into communities using the Louvain algorithm.
|
|
7664
8607
|
|
|
@@ -7682,12 +8625,23 @@ class Graph():
|
|
|
7682
8625
|
randomization_seed : int, optional
|
|
7683
8626
|
The random number generator seed for the run. Must be a
|
|
7684
8627
|
non-negative integer. Default is 8675309.
|
|
8628
|
+
diagnostic_info : bool, optional
|
|
8629
|
+
If ``True``, returns diagnostic information alongside community
|
|
8630
|
+
assignments. If ``False`` (default), returns only community assignments.
|
|
7685
8631
|
|
|
7686
8632
|
Returns
|
|
7687
8633
|
-------
|
|
7688
|
-
Relationship
|
|
7689
|
-
|
|
7690
|
-
community
|
|
8634
|
+
Relationship or tuple of Relationships
|
|
8635
|
+
If ``diagnostic_info`` is ``False`` (default), returns a binary
|
|
8636
|
+
relationship where each tuple represents a node and its community
|
|
8637
|
+
assignment.
|
|
8638
|
+
|
|
8639
|
+
If ``diagnostic_info`` is ``True``, returns a tuple of
|
|
8640
|
+
``(community_assignments, diagnostic_info)`` where:
|
|
8641
|
+
|
|
8642
|
+
- ``community_assignments`` is the binary relationship described above
|
|
8643
|
+
- ``diagnostic_info`` is a unary relationship containing a diagnostic
|
|
8644
|
+
string describing the algorithm's termination behavior.
|
|
7691
8645
|
|
|
7692
8646
|
Raises
|
|
7693
8647
|
------
|
|
@@ -7696,11 +8650,21 @@ class Graph():
|
|
|
7696
8650
|
|
|
7697
8651
|
Relationship Schema
|
|
7698
8652
|
-------------------
|
|
8653
|
+
When ``diagnostic_info=False`` (default):
|
|
8654
|
+
|
|
7699
8655
|
``louvain(node, community_label)``
|
|
7700
8656
|
|
|
7701
8657
|
* **node** (*Node*): The node.
|
|
7702
8658
|
* **community_label** (*Integer*): The label of the community the node
|
|
7703
|
-
|
|
8659
|
+
belongs to.
|
|
8660
|
+
|
|
8661
|
+
When ``diagnostic_info=True``, returns two relationships:
|
|
8662
|
+
|
|
8663
|
+
- ``louvain(node, community_lable)`` as described above; and
|
|
8664
|
+
- ``diagnostic_info(diagnostic_string)``
|
|
8665
|
+
|
|
8666
|
+
* **diagnostic_string** (*String*): A diagnostic string describing the
|
|
8667
|
+
algorithm's convergence and termination behavior.
|
|
7704
8668
|
|
|
7705
8669
|
Supported Graph Types
|
|
7706
8670
|
---------------------
|
|
@@ -7821,12 +8785,39 @@ class Graph():
|
|
|
7821
8785
|
_assert_type("louvain:randomization_seed", randomization_seed, int)
|
|
7822
8786
|
_assert_exclusive_lower_bound("louvain:randomization_seed", randomization_seed, 0)
|
|
7823
8787
|
|
|
7824
|
-
|
|
8788
|
+
# Collect parameters to rel_primitive_louvain,
|
|
8789
|
+
# appropriately typed and ordered for the primitive.
|
|
8790
|
+
louvain_parameters = [
|
|
8791
|
+
float(level_tolerance),
|
|
8792
|
+
float(sweep_tolerance),
|
|
8793
|
+
int(max_levels),
|
|
8794
|
+
int(max_sweeps),
|
|
8795
|
+
int(randomization_seed),
|
|
8796
|
+
]
|
|
8797
|
+
|
|
8798
|
+
# Create louvain community assignment and, if requested,
|
|
8799
|
+
# diagnostic information relationships.
|
|
8800
|
+
_louvain_assignments_rel, _louvain_diagnostic_rel = \
|
|
8801
|
+
self._create_primitive_algorithm_relationship(
|
|
8802
|
+
"louvain", louvain_parameters
|
|
8803
|
+
)
|
|
8804
|
+
|
|
8805
|
+
# Attach tracking information to the community assignment relationship.
|
|
8806
|
+
_louvain_assignments_rel.annotate(annotations.track("graphs", "louvain"))
|
|
8807
|
+
|
|
8808
|
+
# Return either just the community assignments, or both
|
|
8809
|
+
# the community assignments and diagnostic information, per request.
|
|
8810
|
+
if diagnostic_info:
|
|
8811
|
+
return _louvain_assignments_rel, _louvain_diagnostic_rel
|
|
8812
|
+
else:
|
|
8813
|
+
return _louvain_assignments_rel
|
|
7825
8814
|
|
|
8815
|
+
@include_in_docs
|
|
7826
8816
|
def label_propagation(
|
|
7827
8817
|
self,
|
|
7828
8818
|
max_sweeps: int = 20,
|
|
7829
8819
|
randomization_seed: int = 8675309,
|
|
8820
|
+
diagnostic_info: bool = False,
|
|
7830
8821
|
):
|
|
7831
8822
|
"""Partitions nodes into communities using the Label Propagation algorithm.
|
|
7832
8823
|
|
|
@@ -7841,20 +8832,42 @@ class Graph():
|
|
|
7841
8832
|
randomization_seed : int, optional
|
|
7842
8833
|
The random number generator seed for the run. Must be a positive
|
|
7843
8834
|
integer. Default is 8675309.
|
|
8835
|
+
diagnostic_info : bool, optional
|
|
8836
|
+
If ``True``, returns diagnostic information alongside
|
|
8837
|
+
community assignments. If ``False`` (default), returns only community
|
|
8838
|
+
assignments.
|
|
7844
8839
|
|
|
7845
8840
|
Returns
|
|
7846
8841
|
-------
|
|
7847
|
-
Relationship
|
|
7848
|
-
|
|
7849
|
-
community
|
|
8842
|
+
Relationship or tuple of Relationships
|
|
8843
|
+
If ``diagnostic_info`` is ``False`` (default), returns a binary
|
|
8844
|
+
relationship where each tuple represents a node and its community
|
|
8845
|
+
assignment.
|
|
8846
|
+
|
|
8847
|
+
If ``diagnostic_info`` is ``True``, returns a tuple of
|
|
8848
|
+
``(community_assignments, diagnostic_info)`` where:
|
|
8849
|
+
|
|
8850
|
+
- ``community_assignments`` is the binary relationship described above
|
|
8851
|
+
- ``diagnostic_info`` is a unary relationship containing a diagnostic
|
|
8852
|
+
string describing the algorithm's termination behavior.
|
|
7850
8853
|
|
|
7851
8854
|
Relationship Schema
|
|
7852
8855
|
-------------------
|
|
8856
|
+
When ``diagnostic_info=False`` (default):
|
|
8857
|
+
|
|
7853
8858
|
``label_propagation(node, community_label)``
|
|
7854
8859
|
|
|
7855
8860
|
* **node** (*Node*): The node.
|
|
7856
8861
|
* **community_label** (*Integer*): The label of the community the node
|
|
7857
|
-
|
|
8862
|
+
belongs to.
|
|
8863
|
+
|
|
8864
|
+
When ``diagnostic_info=True``, returns two relationships:
|
|
8865
|
+
|
|
8866
|
+
- ``community_assignments(node, community_label)`` as described above; and
|
|
8867
|
+
- ``diagnostic_info(diganostic_string)``
|
|
8868
|
+
|
|
8869
|
+
* **diagnostic_string** (*String*): A diagnostic string describing the
|
|
8870
|
+
algorithm's convergence and termination behavior.
|
|
7858
8871
|
|
|
7859
8872
|
Supported Graph Types
|
|
7860
8873
|
---------------------
|
|
@@ -7966,4 +8979,23 @@ class Graph():
|
|
|
7966
8979
|
_assert_type("label_propagation:randomization_seed", randomization_seed, int)
|
|
7967
8980
|
_assert_exclusive_lower_bound("label_propagation:randomization_seed", randomization_seed, 0)
|
|
7968
8981
|
|
|
7969
|
-
|
|
8982
|
+
# Collect parameters to rel_primitive_async_label_propagation,
|
|
8983
|
+
# appropriately typed and ordered for the primitive.
|
|
8984
|
+
label_propagation_parameters = [
|
|
8985
|
+
int(max_sweeps),
|
|
8986
|
+
int(randomization_seed),
|
|
8987
|
+
]
|
|
8988
|
+
|
|
8989
|
+
# Invoke the helper method that handles normalization,
|
|
8990
|
+
# primitive invocation, and result transformation.
|
|
8991
|
+
_label_propagation_assignments_rel, _label_propagation_diagnostic_rel = \
|
|
8992
|
+
self._create_primitive_algorithm_relationship("label_propagation", label_propagation_parameters)
|
|
8993
|
+
|
|
8994
|
+
# Add tracking annotation.
|
|
8995
|
+
_label_propagation_assignments_rel.annotate(annotations.track("graphs", "label_propagation"))
|
|
8996
|
+
|
|
8997
|
+
# Return based on whether diagnostic information was requested.
|
|
8998
|
+
if diagnostic_info:
|
|
8999
|
+
return _label_propagation_assignments_rel, _label_propagation_diagnostic_rel
|
|
9000
|
+
else:
|
|
9001
|
+
return _label_propagation_assignments_rel
|