relationalai 0.11.4__py3-none-any.whl → 0.12.0__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.
Files changed (31) hide show
  1. relationalai/clients/config.py +7 -0
  2. relationalai/clients/direct_access_client.py +113 -0
  3. relationalai/clients/snowflake.py +35 -106
  4. relationalai/early_access/metamodel/rewrite/__init__.py +5 -3
  5. relationalai/early_access/rel/rewrite/__init__.py +1 -1
  6. relationalai/errors.py +24 -3
  7. relationalai/semantics/internal/annotations.py +1 -0
  8. relationalai/semantics/lqp/builtins.py +1 -0
  9. relationalai/semantics/lqp/passes.py +3 -4
  10. relationalai/semantics/{rel → lqp}/rewrite/__init__.py +6 -0
  11. relationalai/semantics/metamodel/builtins.py +12 -1
  12. relationalai/semantics/metamodel/rewrite/__init__.py +3 -9
  13. relationalai/semantics/reasoners/graph/core.py +221 -71
  14. relationalai/semantics/rel/builtins.py +5 -1
  15. relationalai/semantics/rel/compiler.py +3 -3
  16. relationalai/semantics/sql/compiler.py +2 -3
  17. relationalai/semantics/sql/executor/duck_db.py +8 -4
  18. relationalai/tools/cli.py +11 -4
  19. {relationalai-0.11.4.dist-info → relationalai-0.12.0.dist-info}/METADATA +5 -4
  20. {relationalai-0.11.4.dist-info → relationalai-0.12.0.dist-info}/RECORD +29 -30
  21. relationalai/semantics/metamodel/rewrite/gc_nodes.py +0 -58
  22. relationalai/semantics/metamodel/rewrite/list_types.py +0 -109
  23. /relationalai/semantics/{rel → lqp}/rewrite/cdc.py +0 -0
  24. /relationalai/semantics/{rel → lqp}/rewrite/extract_common.py +0 -0
  25. /relationalai/semantics/{metamodel → lqp}/rewrite/extract_keys.py +0 -0
  26. /relationalai/semantics/{metamodel → lqp}/rewrite/fd_constraints.py +0 -0
  27. /relationalai/semantics/{rel → lqp}/rewrite/quantify_vars.py +0 -0
  28. /relationalai/semantics/{metamodel → lqp}/rewrite/splinter.py +0 -0
  29. {relationalai-0.11.4.dist-info → relationalai-0.12.0.dist-info}/WHEEL +0 -0
  30. {relationalai-0.11.4.dist-info → relationalai-0.12.0.dist-info}/entry_points.txt +0 -0
  31. {relationalai-0.11.4.dist-info → relationalai-0.12.0.dist-info}/licenses/LICENSE +0 -0
@@ -20,6 +20,7 @@ from relationalai.semantics import (
20
20
  count, sum, avg,
21
21
  )
22
22
  from relationalai.docutils import include_in_docs
23
+ from relationalai.semantics.internal import annotations
23
24
  from relationalai.semantics.std.math import abs, isnan, isinf, maximum, natural_log, sqrt
24
25
 
25
26
  Numeric = Union[int, float, Decimal]
@@ -1250,6 +1251,8 @@ class Graph():
1250
1251
  def _num_nodes(self):
1251
1252
  """Lazily define and cache the self._num_nodes relationship."""
1252
1253
  _num_nodes_rel = self._model.Relationship("The graph has {num_nodes:Integer} nodes")
1254
+ _num_nodes_rel.annotate(annotations.track("graphs", "num_nodes"))
1255
+
1253
1256
  define(_num_nodes_rel(count(self.Node) | 0))
1254
1257
  return _num_nodes_rel
1255
1258
 
@@ -1316,6 +1319,7 @@ class Graph():
1316
1319
  def _num_edges(self):
1317
1320
  """Lazily define and cache the self._num_edges relationship."""
1318
1321
  _num_edges_rel = self._model.Relationship("The graph has {num_edges:Integer} edges")
1322
+ _num_edges_rel.annotate(annotations.track("graphs", "num_edges"))
1319
1323
 
1320
1324
  src, dst = self.Node.ref(), self.Node.ref()
1321
1325
  if self.directed:
@@ -1468,7 +1472,9 @@ class Graph():
1468
1472
  @cached_property
1469
1473
  def _neighbor(self):
1470
1474
  """Lazily define and cache the self._neighbor relationship."""
1471
- return self._create_neighbor_relationship(nodes_subset=None)
1475
+ _neighbor_rel = self._create_neighbor_relationship(nodes_subset=None)
1476
+ _neighbor_rel.annotate(annotations.track("graphs", "neighbor"))
1477
+ return _neighbor_rel
1472
1478
 
1473
1479
  def _neighbor_of(self, nodes_subset: Relationship):
1474
1480
  """
@@ -1476,7 +1482,9 @@ class Graph():
1476
1482
  in `nodes_subset`. Note this relationship is not cached; it is
1477
1483
  specific to the callsite.
1478
1484
  """
1479
- return self._create_neighbor_relationship(nodes_subset=nodes_subset)
1485
+ _neighbor_rel = self._create_neighbor_relationship(nodes_subset=nodes_subset)
1486
+ _neighbor_rel.annotate(annotations.track("graphs", "neighbor_of"))
1487
+ return _neighbor_rel
1480
1488
 
1481
1489
  def _create_neighbor_relationship(self, *, nodes_subset: Optional[Relationship]):
1482
1490
  _neighbor_rel = self._model.Relationship(f"{{src:{self._NodeConceptStr}}} has neighbor {{dst:{self._NodeConceptStr}}}")
@@ -1626,7 +1634,9 @@ class Graph():
1626
1634
  @cached_property
1627
1635
  def _inneighbor(self):
1628
1636
  """Lazily define and cache the self._inneighbor relationship."""
1629
- return self._create_inneighbor_relationship(nodes_subset=None)
1637
+ _inneighbor_rel = self._create_inneighbor_relationship(nodes_subset=None)
1638
+ _inneighbor_rel.annotate(annotations.track("graphs", "inneighbor"))
1639
+ return _inneighbor_rel
1630
1640
 
1631
1641
  def _inneighbor_of(self, nodes_subset: Relationship):
1632
1642
  """
@@ -1634,7 +1644,9 @@ class Graph():
1634
1644
  in `nodes_subset`. Note this relationship is not cached; it is
1635
1645
  specific to the callsite.
1636
1646
  """
1637
- return self._create_inneighbor_relationship(nodes_subset=nodes_subset)
1647
+ _inneighbor_rel = self._create_inneighbor_relationship(nodes_subset=nodes_subset)
1648
+ _inneighbor_rel.annotate(annotations.track("graphs", "inneighbor_of"))
1649
+ return _inneighbor_rel
1638
1650
 
1639
1651
  def _create_inneighbor_relationship(self, *, nodes_subset: Optional[Relationship]):
1640
1652
  _inneighbor_rel = self._model.Relationship(f"{{dst:{self._NodeConceptStr}}} has inneighbor {{src:{self._NodeConceptStr}}}")
@@ -1777,7 +1789,9 @@ class Graph():
1777
1789
  @cached_property
1778
1790
  def _outneighbor(self):
1779
1791
  """Lazily define and cache the self._outneighbor relationship."""
1780
- return self._create_outneighbor_relationship(nodes_subset=None)
1792
+ _outneighbor_rel = self._create_outneighbor_relationship(nodes_subset=None)
1793
+ _outneighbor_rel.annotate(annotations.track("graphs", "outneighbor"))
1794
+ return _outneighbor_rel
1781
1795
 
1782
1796
  def _outneighbor_of(self, nodes_subset: Relationship):
1783
1797
  """
@@ -1785,7 +1799,9 @@ class Graph():
1785
1799
  in `nodes_subset`. Note this relationship is not cached; it is
1786
1800
  specific to the callsite.
1787
1801
  """
1788
- return self._create_outneighbor_relationship(nodes_subset=nodes_subset)
1802
+ _outneighbor_rel = self._create_outneighbor_relationship(nodes_subset=nodes_subset)
1803
+ _outneighbor_rel.annotate(annotations.track("graphs", "outneighbor_of"))
1804
+ return _outneighbor_rel
1789
1805
 
1790
1806
  def _create_outneighbor_relationship(self, *, nodes_subset: Optional[Relationship]):
1791
1807
  _outneighbor_rel = self._model.Relationship(f"{{src:{self._NodeConceptStr}}} has outneighbor {{dst:{self._NodeConceptStr}}}")
@@ -1914,6 +1930,8 @@ class Graph():
1914
1930
  def _common_neighbor(self):
1915
1931
  """Lazily define and cache the self._common_neighbor relationship."""
1916
1932
  _common_neighbor_rel = self._model.Relationship(f"{{node_a:{self._NodeConceptStr}}} and {{node_b:{self._NodeConceptStr}}} have common neighbor {{node_c:{self._NodeConceptStr}}}")
1933
+ _common_neighbor_rel.annotate(annotations.track("graphs", "common_neighbor"))
1934
+
1917
1935
  node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
1918
1936
  where(self._neighbor(node_a, node_c), self._neighbor(node_b, node_c)).define(_common_neighbor_rel(node_a, node_b, node_c))
1919
1937
  return _common_neighbor_rel
@@ -2072,7 +2090,9 @@ class Graph():
2072
2090
  @cached_property
2073
2091
  def _degree(self):
2074
2092
  """Lazily define and cache the self._degree relationship."""
2075
- return self._create_degree_relationship(nodes_subset=None)
2093
+ _degree_rel = self._create_degree_relationship(nodes_subset=None)
2094
+ _degree_rel.annotate(annotations.track("graphs", "degree"))
2095
+ return _degree_rel
2076
2096
 
2077
2097
  def _degree_of(self, nodes_subset: Relationship):
2078
2098
  """
@@ -2080,7 +2100,9 @@ class Graph():
2080
2100
  in `nodes_subset`. Note this relationship is not cached; it is
2081
2101
  specific to the callsite.
2082
2102
  """
2083
- return self._create_degree_relationship(nodes_subset=nodes_subset)
2103
+ _degree_rel = self._create_degree_relationship(nodes_subset=nodes_subset)
2104
+ _degree_rel.annotate(annotations.track("graphs", "degree_of"))
2105
+ return _degree_rel
2084
2106
 
2085
2107
  def _create_degree_relationship(self, *, nodes_subset: Optional[Relationship]):
2086
2108
  _degree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has degree {{count:Integer}}")
@@ -2263,7 +2285,9 @@ class Graph():
2263
2285
  @cached_property
2264
2286
  def _indegree(self):
2265
2287
  """Lazily define and cache the self._indegree relationship."""
2266
- return self._create_indegree_relationship(nodes_subset=None)
2288
+ _indegree_rel = self._create_indegree_relationship(nodes_subset=None)
2289
+ _indegree_rel.annotate(annotations.track("graphs", "indegree"))
2290
+ return _indegree_rel
2267
2291
 
2268
2292
  def _indegree_of(self, nodes_subset: Relationship):
2269
2293
  """
@@ -2271,7 +2295,9 @@ class Graph():
2271
2295
  in `nodes_subset`. Note this relationship is not cached; it is
2272
2296
  specific to the callsite.
2273
2297
  """
2274
- return self._create_indegree_relationship(nodes_subset=nodes_subset)
2298
+ _indegree_rel = self._create_indegree_relationship(nodes_subset=nodes_subset)
2299
+ _indegree_rel.annotate(annotations.track("graphs", "indegree_of"))
2300
+ return _indegree_rel
2275
2301
 
2276
2302
  def _create_indegree_relationship(self, *, nodes_subset: Optional[Relationship]):
2277
2303
  _indegree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has indegree {{count:Integer}}")
@@ -2443,7 +2469,9 @@ class Graph():
2443
2469
  @cached_property
2444
2470
  def _outdegree(self):
2445
2471
  """Lazily define and cache the self._outdegree relationship."""
2446
- return self._create_outdegree_relationship(nodes_subset=None)
2472
+ _outdegree_rel = self._create_outdegree_relationship(nodes_subset=None)
2473
+ _outdegree_rel.annotate(annotations.track("graphs", "outdegree"))
2474
+ return _outdegree_rel
2447
2475
 
2448
2476
  def _outdegree_of(self, nodes_subset: Relationship):
2449
2477
  """
@@ -2451,7 +2479,9 @@ class Graph():
2451
2479
  in `nodes_subset`. Note this relationship is not cached; it is
2452
2480
  specific to the callsite.
2453
2481
  """
2454
- return self._create_outdegree_relationship(nodes_subset=nodes_subset)
2482
+ _outdegree_rel = self._create_outdegree_relationship(nodes_subset=nodes_subset)
2483
+ _outdegree_rel.annotate(annotations.track("graphs", "outdegree_of"))
2484
+ return _outdegree_rel
2455
2485
 
2456
2486
  def _create_outdegree_relationship(self, *, nodes_subset: Optional[Relationship]):
2457
2487
  _outdegree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has outdegree {{count:Integer}}")
@@ -2588,7 +2618,9 @@ class Graph():
2588
2618
  @cached_property
2589
2619
  def _weighted_degree(self):
2590
2620
  """Lazily define and cache the self._weighted_degree relationship."""
2591
- return self._create_weighted_degree_relationship(nodes_subset=None)
2621
+ _weighted_degree_rel = self._create_weighted_degree_relationship(nodes_subset=None)
2622
+ _weighted_degree_rel.annotate(annotations.track("graphs", "weighted_degree"))
2623
+ return _weighted_degree_rel
2592
2624
 
2593
2625
  def _weighted_degree_of(self, nodes_subset: Relationship):
2594
2626
  """
@@ -2596,7 +2628,9 @@ class Graph():
2596
2628
  in `nodes_subset`. Note this relationship is not cached; it is
2597
2629
  specific to the callsite.
2598
2630
  """
2599
- return self._create_weighted_degree_relationship(nodes_subset=nodes_subset)
2631
+ _weighted_degree_rel = self._create_weighted_degree_relationship(nodes_subset=nodes_subset)
2632
+ _weighted_degree_rel.annotate(annotations.track("graphs", "weighted_degree_of"))
2633
+ return _weighted_degree_rel
2600
2634
 
2601
2635
  def _create_weighted_degree_relationship(self, *, nodes_subset: Optional[Relationship]):
2602
2636
  _weighted_degree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has weighted degree {{weight:Float}}")
@@ -2744,7 +2778,9 @@ class Graph():
2744
2778
  @cached_property
2745
2779
  def _weighted_indegree(self):
2746
2780
  """Lazily define and cache the self._weighted_indegree relationship."""
2747
- return self._create_weighted_indegree_relationship(nodes_subset=None)
2781
+ _weighted_indegree_rel = self._create_weighted_indegree_relationship(nodes_subset=None)
2782
+ _weighted_indegree_rel.annotate(annotations.track("graphs", "weighted_indegree"))
2783
+ return _weighted_indegree_rel
2748
2784
 
2749
2785
  def _weighted_indegree_of(self, nodes_subset: Relationship):
2750
2786
  """
@@ -2752,7 +2788,9 @@ class Graph():
2752
2788
  in `nodes_subset`. Note this relationship is not cached; it is
2753
2789
  specific to the callsite.
2754
2790
  """
2755
- return self._create_weighted_indegree_relationship(nodes_subset=nodes_subset)
2791
+ _weighted_indegree_rel = self._create_weighted_indegree_relationship(nodes_subset=nodes_subset)
2792
+ _weighted_indegree_rel.annotate(annotations.track("graphs", "weighted_indegree_of"))
2793
+ return _weighted_indegree_rel
2756
2794
 
2757
2795
  def _create_weighted_indegree_relationship(self, *, nodes_subset: Optional[Relationship]):
2758
2796
  _weighted_indegree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has weighted indegree {{weight:Float}}")
@@ -2892,7 +2930,9 @@ class Graph():
2892
2930
  @cached_property
2893
2931
  def _weighted_outdegree(self):
2894
2932
  """Lazily define and cache the self._weighted_outdegree relationship."""
2895
- return self._create_weighted_outdegree_relationship(nodes_subset=None)
2933
+ _weighted_outdegree_rel = self._create_weighted_outdegree_relationship(nodes_subset=None)
2934
+ _weighted_outdegree_rel.annotate(annotations.track("graphs", "weighted_outdegree"))
2935
+ return _weighted_outdegree_rel
2896
2936
 
2897
2937
  def _weighted_outdegree_of(self, nodes_subset: Relationship):
2898
2938
  """
@@ -2900,7 +2940,9 @@ class Graph():
2900
2940
  in `nodes_subset`. Note this relationship is not cached; it is
2901
2941
  specific to the callsite.
2902
2942
  """
2903
- return self._create_weighted_outdegree_relationship(nodes_subset=nodes_subset)
2943
+ _weighted_outdegree_rel = self._create_weighted_outdegree_relationship(nodes_subset=nodes_subset)
2944
+ _weighted_outdegree_rel.annotate(annotations.track("graphs", "weighted_outdegree_of"))
2945
+ return _weighted_outdegree_rel
2904
2946
 
2905
2947
  def _create_weighted_outdegree_relationship(self, *, nodes_subset: Optional[Relationship]):
2906
2948
  _weighted_outdegree_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has weighted outdegree {{weight:Float}}")
@@ -3067,7 +3109,9 @@ class Graph():
3067
3109
  @cached_property
3068
3110
  def _degree_centrality(self):
3069
3111
  """Lazily define and cache the self._degree_centrality relationship."""
3070
- return self._create_degree_centrality_relationship(nodes_subset=None)
3112
+ _degree_centrality_rel = self._create_degree_centrality_relationship(nodes_subset=None)
3113
+ _degree_centrality_rel.annotate(annotations.track("graphs", "degree_centrality"))
3114
+ return _degree_centrality_rel
3071
3115
 
3072
3116
  def _degree_centrality_of(self, nodes_subset: Relationship):
3073
3117
  """
@@ -3075,7 +3119,9 @@ class Graph():
3075
3119
  in `nodes_subset`. Note this relationship is not cached; it is
3076
3120
  specific to the callsite.
3077
3121
  """
3078
- return self._create_degree_centrality_relationship(nodes_subset=nodes_subset)
3122
+ _degree_centrality_rel = self._create_degree_centrality_relationship(nodes_subset=nodes_subset)
3123
+ _degree_centrality_rel.annotate(annotations.track("graphs", "degree_centrality_of"))
3124
+ return _degree_centrality_rel
3079
3125
 
3080
3126
  def _create_degree_centrality_relationship(self, *, nodes_subset: Optional[Relationship]):
3081
3127
  """Create a degree centrality relationship, optionally constrained to a subset of nodes."""
@@ -3572,6 +3618,7 @@ class Graph():
3572
3618
  def _triangle(self):
3573
3619
  """Lazily define and cache the self._triangle relationship."""
3574
3620
  _triangle_rel = self._model.Relationship(f"{{node_a:{self._NodeConceptStr}}} and {{node_b:{self._NodeConceptStr}}} and {{node_c:{self._NodeConceptStr}}} form a triangle")
3621
+ _triangle_rel.annotate(annotations.track("graphs", "triangle"))
3575
3622
 
3576
3623
  a, b, c = self.Node.ref(), self.Node.ref(), self.Node.ref()
3577
3624
 
@@ -3714,6 +3761,7 @@ class Graph():
3714
3761
  def _unique_triangle(self):
3715
3762
  """Lazily define and cache the self._unique_triangle relationship."""
3716
3763
  _unique_triangle_rel = self._model.Relationship(f"{{node_a:{self._NodeConceptStr}}} and {{node_b:{self._NodeConceptStr}}} and {{node_c:{self._NodeConceptStr}}} form unique triangle")
3764
+ _unique_triangle_rel.annotate(annotations.track("graphs", "unique_triangle"))
3717
3765
 
3718
3766
  node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
3719
3767
 
@@ -3849,6 +3897,7 @@ class Graph():
3849
3897
  def _num_triangles(self):
3850
3898
  """Lazily define and cache the self._num_triangles relationship."""
3851
3899
  _num_triangles_rel = self._model.Relationship("The graph has {num_triangles:Integer} triangles")
3900
+ _num_triangles_rel.annotate(annotations.track("graphs", "num_triangles"))
3852
3901
 
3853
3902
  _num_triangles = Integer.ref()
3854
3903
  node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
@@ -3973,7 +4022,9 @@ class Graph():
3973
4022
  @cached_property
3974
4023
  def _triangle_count(self):
3975
4024
  """Lazily define and cache the self._triangle_count relationship."""
3976
- return self._create_triangle_count_relationship(nodes_subset=None)
4025
+ _triangle_count_rel = self._create_triangle_count_relationship(nodes_subset=None)
4026
+ _triangle_count_rel.annotate(annotations.track("graphs", "triangle_count"))
4027
+ return _triangle_count_rel
3977
4028
 
3978
4029
  def _triangle_count_of(self, nodes_subset: Relationship):
3979
4030
  """
@@ -3981,7 +4032,9 @@ class Graph():
3981
4032
  in `nodes_subset`. Note this relationship is not cached; it is
3982
4033
  specific to the callsite.
3983
4034
  """
3984
- return self._create_triangle_count_relationship(nodes_subset=nodes_subset)
4035
+ _triangle_count_rel = self._create_triangle_count_relationship(nodes_subset=nodes_subset)
4036
+ _triangle_count_rel.annotate(annotations.track("graphs", "triangle_count_of"))
4037
+ return _triangle_count_rel
3985
4038
 
3986
4039
  def _create_triangle_count_relationship(self, *, nodes_subset: Optional[Relationship]):
3987
4040
  """Create a triangle count relationship, optionally constrained to a subset of nodes."""
@@ -4113,7 +4166,7 @@ class Graph():
4113
4166
 
4114
4167
 
4115
4168
  @include_in_docs
4116
- def local_clustering_coefficient(self):
4169
+ def local_clustering_coefficient(self, *, of: Optional[Relationship] = None):
4117
4170
  """Returns a binary relationship containing the local clustering coefficient of each node.
4118
4171
 
4119
4172
  The local clustering coefficient quantifies how close a node's neighbors
@@ -4122,6 +4175,14 @@ class Graph():
4122
4175
  directly connecting them, and 1.0 indicates all neighbors have edges
4123
4176
  directly connecting them.
4124
4177
 
4178
+ Parameters
4179
+ ----------
4180
+ of : Relationship, optional
4181
+ A unary relationship containing a subset of the graph's nodes. When
4182
+ provided, constrains the domain of the local clustering coefficient
4183
+ computation: only coefficients of nodes in this relationship are
4184
+ computed and returned.
4185
+
4125
4186
  Returns
4126
4187
  -------
4127
4188
  Relationship
@@ -4148,17 +4209,6 @@ class Graph():
4148
4209
  | Directed | No | Undirected only. |
4149
4210
  | Weighted | Yes | Weights are ignored. |
4150
4211
 
4151
- Notes
4152
- -----
4153
- The formal definition of the local clustering coefficient (`C`) for a
4154
- node (`v`) can be given as::
4155
-
4156
- C(v) = (2 * num_edges) / (degree(v) * (degree(v) - 1))
4157
-
4158
- Here, `num_edges` represents the number of edges between the
4159
- neighbors of node `v`, and `degree(v)` represents the degree of the
4160
- node, i.e., the number of edges connected to the node.
4161
-
4162
4212
  Examples
4163
4213
  --------
4164
4214
  >>> from relationalai.semantics import Model, define, select, Float
@@ -4194,6 +4244,41 @@ class Graph():
4194
4244
  3 4 0.333333
4195
4245
  4 5 0.000000
4196
4246
 
4247
+ >>> # 4. Use 'of' parameter to constrain the set of nodes to compute local clustering coefficients of
4248
+ >>> # Define a subset containing only nodes 1 and 3
4249
+ >>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
4250
+ >>> node = Node.ref()
4251
+ >>> where(union(node.id == 1, node.id == 3)).define(subset(node))
4252
+ >>>
4253
+ >>> # Get local clustering coefficients only of nodes in the subset
4254
+ >>> constrained_lcc = graph.local_clustering_coefficient(of=subset)
4255
+ >>> select(node.id, coeff).where(constrained_lcc(node, coeff)).inspect()
4256
+ ▰▰▰▰ Setup complete
4257
+ id coeff
4258
+ 0 1 1.000000
4259
+ 1 3 0.666667
4260
+
4261
+ Notes
4262
+ -----
4263
+ The local clustering coefficient for node `v` is::
4264
+
4265
+ (2 * num_neighbor_edges(v)) / (ext_degree(v) * (ext_degree(v) - 1))
4266
+
4267
+ where `num_neighbor_edges(v)` is the number of edges between
4268
+ the neighbors of node `v`, and `ext_degree(v)` is the degree of the
4269
+ node excluding self-loops. If `ext_degree(v)` is less than 2,
4270
+ the local clustering coefficient is 0.0.
4271
+
4272
+ The ``local_clustering_coefficient()`` method, called with no parameters, computes
4273
+ and caches the full local clustering coefficient relationship, providing efficient
4274
+ reuse across multiple calls to ``local_clustering_coefficient()``. In contrast,
4275
+ ``local_clustering_coefficient(of=subset)`` computes a constrained relationship
4276
+ specific to the passed-in ``subset`` and that call site. When a significant fraction
4277
+ of the local clustering coefficient relation is needed across a program,
4278
+ ``local_clustering_coefficient()`` is typically more efficient; this is the typical
4279
+ case. Use ``local_clustering_coefficient(of=subset)`` only when small subsets of the
4280
+ local clustering coefficient relationship are needed collectively across the program.
4281
+
4197
4282
 
4198
4283
  See Also
4199
4284
  --------
@@ -4206,29 +4291,51 @@ class Graph():
4206
4291
  raise NotImplementedError(
4207
4292
  "`local_clustering_coefficient` is not applicable to directed graphs"
4208
4293
  )
4294
+
4295
+ if of is not None:
4296
+ self._validate_node_subset_parameter(of)
4297
+ return self._local_clustering_coefficient_of(of)
4209
4298
  return self._local_clustering_coefficient
4210
4299
 
4211
4300
  @cached_property
4212
4301
  def _local_clustering_coefficient(self):
4302
+ """Lazily define and cache the self._local_clustering_coefficient relationship."""
4303
+ _local_clustering_coefficient_rel = self._create_local_clustering_coefficient_relationship(nodes_subset=None)
4304
+ _local_clustering_coefficient_rel.annotate(annotations.track("graphs", "local_clustering_coefficient"))
4305
+ return _local_clustering_coefficient_rel
4306
+
4307
+ def _local_clustering_coefficient_of(self, nodes_subset: Relationship):
4213
4308
  """
4214
- Lazily define and cache the self._local_clustering_coefficient relationship,
4215
- which only applies to undirected graphs.
4309
+ Create a local clustering coefficient relationship constrained to the subset of nodes
4310
+ in `nodes_subset`. Note this relationship is not cached; it is
4311
+ specific to the callsite.
4216
4312
  """
4217
- _local_clustering_coefficient_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has local clustering coefficient {{coefficient:Float}}")
4313
+ _local_clustering_coefficient_rel = self._create_local_clustering_coefficient_relationship(nodes_subset=nodes_subset)
4314
+ _local_clustering_coefficient_rel.annotate(annotations.track("graphs", "local_clustering_coefficient_of"))
4315
+ return _local_clustering_coefficient_rel
4218
4316
 
4219
- if self.directed:
4220
- raise NotImplementedError(
4221
- "`local_clustering_coefficient is not defined for directed graphs."
4222
- )
4317
+ def _create_local_clustering_coefficient_relationship(self, *, nodes_subset: Optional[Relationship]):
4318
+ """Create a local clustering coefficient relationship, optionally constrained to a subset of nodes."""
4319
+ _local_clustering_coefficient_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has local clustering coefficient {{coefficient:Float}}")
4223
4320
 
4224
4321
  node = self.Node.ref()
4322
+
4323
+ if nodes_subset is None:
4324
+ degree_no_self_rel = self._degree_no_self
4325
+ triangle_count_rel = self._triangle_count
4326
+ node_constraint = node # No constraint on nodes.
4327
+ else:
4328
+ degree_no_self_rel = self._degree_no_self_of(nodes_subset)
4329
+ triangle_count_rel = self._triangle_count_of(nodes_subset)
4330
+ node_constraint = nodes_subset(node) # Nodes constrained to given subset.
4331
+
4225
4332
  degree_no_self = Integer.ref()
4226
4333
  triangle_count = Integer.ref()
4227
4334
  where(
4228
- node,
4335
+ node_constraint,
4229
4336
  _lcc := where(
4230
- self._degree_no_self(node, degree_no_self),
4231
- self._triangle_count(node, triangle_count),
4337
+ degree_no_self_rel(node, degree_no_self),
4338
+ triangle_count_rel(node, triangle_count),
4232
4339
  degree_no_self > 1
4233
4340
  ).select(
4234
4341
  2.0 * triangle_count / (degree_no_self * (degree_no_self - 1.0))
@@ -4243,11 +4350,32 @@ class Graph():
4243
4350
  Lazily define and cache the self._degree_no_self relationship,
4244
4351
  a non-public helper for local_clustering_coefficient.
4245
4352
  """
4353
+ return self._create_degree_no_self_relationship(nodes_subset=None)
4354
+
4355
+ def _degree_no_self_of(self, nodes_subset: Relationship):
4356
+ """
4357
+ Create a self-loop-exclusive degree relationship constrained to
4358
+ the subset of nodes in `nodes_subset`. Note this relationship
4359
+ is not cached; it is specific to the callsite.
4360
+ """
4361
+ return self._create_degree_no_self_relationship(nodes_subset=nodes_subset)
4362
+
4363
+ def _create_degree_no_self_relationship(self, *, nodes_subset: Optional[Relationship]):
4364
+ """
4365
+ Create a self-loop-exclusive degree relationship,
4366
+ optionally constrained to a subset of nodes.
4367
+ """
4246
4368
  _degree_no_self_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has degree excluding self loops {{num:Integer}}")
4247
4369
 
4248
4370
  node, neighbor = self.Node.ref(), self.Node.ref()
4371
+
4372
+ if nodes_subset is None:
4373
+ node_constraint = node # No constraint on nodes.
4374
+ else:
4375
+ node_constraint = nodes_subset(node) # Nodes constrained to given subset.
4376
+
4249
4377
  where(
4250
- self.Node(node),
4378
+ node_constraint,
4251
4379
  _dns := count(neighbor).per(node).where(self._no_loop_edge(node, neighbor)) | 0,
4252
4380
  ).define(_degree_no_self_rel(node, _dns))
4253
4381
 
@@ -4331,6 +4459,7 @@ class Graph():
4331
4459
  which only applies to undirected graphs.
4332
4460
  """
4333
4461
  _average_clustering_coefficient_rel = self._model.Relationship("The graph has average clustering coefficient {{coefficient:Float}}")
4462
+ _average_clustering_coefficient_rel.annotate(annotations.track("graphs", "average_clustering_coefficient"))
4334
4463
 
4335
4464
  if self.directed:
4336
4465
  raise NotImplementedError(
@@ -4471,6 +4600,7 @@ class Graph():
4471
4600
  def _reachable_from(self):
4472
4601
  """Lazily define and cache the self._reachable_from relationship."""
4473
4602
  _reachable_from_rel = self._model.Relationship(f"{{node_a:{self._NodeConceptStr}}} reaches {{node_b:{self._NodeConceptStr}}}")
4603
+ _reachable_from_rel.annotate(annotations.track("graphs", "reachable_from"))
4474
4604
 
4475
4605
  node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
4476
4606
  define(_reachable_from_rel(node_a, node_a))
@@ -4613,9 +4743,12 @@ class Graph():
4613
4743
  def _distance(self):
4614
4744
  """Lazily define and cache the self._distance relationship."""
4615
4745
  if not self.weighted:
4616
- return self._distance_non_weighted
4746
+ _distance_rel = self._distance_non_weighted
4617
4747
  else:
4618
- return self._distance_weighted
4748
+ _distance_rel = self._distance_weighted
4749
+
4750
+ _distance_rel.annotate(annotations.track("graphs", "distance"))
4751
+ return _distance_rel
4619
4752
 
4620
4753
  @cached_property
4621
4754
  def _distance_weighted(self):
@@ -4741,6 +4874,7 @@ class Graph():
4741
4874
  def _weakly_connected_component(self):
4742
4875
  """Lazily define and cache the self._weakly_connected_component relationship."""
4743
4876
  _weakly_connected_component_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} is in the connected component {{id:{self._NodeConceptStr}}}")
4877
+ _weakly_connected_component_rel.annotate(annotations.track("graphs", "weakly_connected_component"))
4744
4878
 
4745
4879
  node, node_v, component = self.Node.ref(), self.Node.ref(), self.Node.ref()
4746
4880
  node, component = union(
@@ -4864,6 +4998,8 @@ class Graph():
4864
4998
  """
4865
4999
  _diameter_range_min_rel = self._model.Relationship("The graph has a min diameter range of {value:Integer}")
4866
5000
  _diameter_range_max_rel = self._model.Relationship("The graph has a max diameter range of {value:Integer}")
5001
+ _diameter_range_min_rel.annotate(annotations.track("graphs", "diameter_range_min"))
5002
+ _diameter_range_max_rel.annotate(annotations.track("graphs", "diameter_range_max"))
4867
5003
 
4868
5004
  component_node_pairs = self._model.Relationship(f"component id {{cid:{self._NodeConceptStr}}} has node id {{nid:{self._NodeConceptStr}}}")
4869
5005
  nodeid, cid, degreevalue = self.Node.ref(), self.Node.ref(), Integer.ref()
@@ -4924,16 +5060,22 @@ class Graph():
4924
5060
 
4925
5061
  @include_in_docs
4926
5062
  def is_connected(self):
4927
- """Returns a query fragment that is satisfied if the graph is connected.
5063
+ """Returns a unary relationship containing whether the graph is connected.
4928
5064
 
4929
5065
  A graph is considered connected if every node is reachable from every
4930
5066
  other node in the underlying undirected graph.
4931
5067
 
4932
5068
  Returns
4933
5069
  -------
4934
- Fragment
4935
- A query fragment that can be used as a condition in other
4936
- queries to assert that the graph is connected.
5070
+ Relationship
5071
+ A unary relationship containing a boolean indicator of whether the graph
5072
+ is connected.
5073
+
5074
+ Relationship Schema
5075
+ -------------------
5076
+ ``is_connected(connected)``
5077
+
5078
+ * **connected** (*Boolean*): Whether the graph is connected.
4937
5079
 
4938
5080
  Supported Graph Types
4939
5081
  ---------------------
@@ -4951,8 +5093,6 @@ class Graph():
4951
5093
  --------
4952
5094
  **Connected Graph Example**
4953
5095
 
4954
- The following query will produce a result because the graph is connected.
4955
-
4956
5096
  >>> from relationalai.semantics import Model, define, select
4957
5097
  >>> from relationalai.semantics.reasoners.graph import Graph
4958
5098
  >>>
@@ -4970,17 +5110,14 @@ class Graph():
4970
5110
  ... Edge.new(src=n4, dst=n3),
4971
5111
  ... )
4972
5112
  >>>
4973
- >>> # 3. Use the fragment as a condition in a query
4974
- >>> select("Graph is connected").where(graph.is_connected()).inspect()
5113
+ >>> # 3. Select and inspect the relation
5114
+ >>> select(graph.is_connected()).inspect()
4975
5115
  ▰▰▰▰ Setup complete
4976
- v
4977
- 0 Graph is connected
5116
+ is_connected
5117
+ 0 True
4978
5118
 
4979
5119
  **Disconnected Graph Example**
4980
5120
 
4981
- The following query will produce no results because the graph is not
4982
- connected.
4983
-
4984
5121
  >>> from relationalai.semantics import Model, define, select
4985
5122
  >>> from relationalai.semantics.reasoners.graph import Graph
4986
5123
  >>>
@@ -4998,22 +5135,31 @@ class Graph():
4998
5135
  ... Edge.new(src=n4, dst=n5), # This edge creates a separate component
4999
5136
  ... )
5000
5137
  >>>
5001
- >>> # 3. The conditional query produces no output
5002
- >>> select("Graph is connected").where(graph.is_connected()).inspect()
5138
+ >>> # 3. Select and inspect the relation
5139
+ >>> select(graph.is_connected()).inspect()
5003
5140
  ▰▰▰▰ Setup complete
5004
- Empty DataFrame
5005
- Columns: []
5006
- Index: []
5141
+ is_connected
5142
+ 0 False
5007
5143
 
5008
5144
  """
5009
- # TODO (dba) This method is inconsistent with the other,
5010
- # public methods. It does not return a `Relationship`. Revisit
5011
- # this. See GH thread:
5012
- # https://github.com/RelationalAI/relationalai-python/pull/2077#discussion_r2190538074
5013
- return where(
5145
+ return self._is_connected
5146
+
5147
+ @cached_property
5148
+ def _is_connected(self):
5149
+ """Lazily define and cache the self._is_connected relationship."""
5150
+ _is_connected_rel = self._model.Relationship("'The graph is connected' is {is_connected:Boolean}")
5151
+ _is_connected_rel.annotate(annotations.track("graphs", "is_connected"))
5152
+
5153
+ where(
5014
5154
  self._num_nodes(0) |
5015
5155
  count(self._reachable_from_min_node(self.Node.ref())) == self._num_nodes(Integer.ref())
5016
- )
5156
+ ).define(_is_connected_rel(True))
5157
+
5158
+ where(
5159
+ not_(_is_connected_rel(True))
5160
+ ).define(_is_connected_rel(False))
5161
+
5162
+ return _is_connected_rel
5017
5163
 
5018
5164
 
5019
5165
  @include_in_docs
@@ -5179,6 +5325,7 @@ class Graph():
5179
5325
  def _jaccard_similarity(self):
5180
5326
  """Lazily define and cache the self._jaccard_similarity relationship."""
5181
5327
  _jaccard_similarity_rel = self._model.Relationship(f"{{node_u:{self._NodeConceptStr}}} has a similarity to {{node_v:{self._NodeConceptStr}}} of {{similarity:Float}}")
5328
+ _jaccard_similarity_rel.annotate(annotations.track("graphs", "jaccard_similarity"))
5182
5329
 
5183
5330
  if not self.weighted:
5184
5331
  node_u, node_v = self.Node.ref(), self.Node.ref()
@@ -5430,6 +5577,7 @@ class Graph():
5430
5577
  def _cosine_similarity(self):
5431
5578
  """Lazily define and cache the self._cosine_similarity relationship."""
5432
5579
  _cosine_similarity_rel = self._model.Relationship(f"{{node_u:{self._NodeConceptStr}}} has a cosine similarity to {{node_v:{self._NodeConceptStr}}} of {{score:Float}}")
5580
+ _cosine_similarity_rel.annotate(annotations.track("graphs", "cosine_similarity"))
5433
5581
 
5434
5582
  if not self.weighted:
5435
5583
  node_u, node_v = self.Node.ref(), self.Node.ref()
@@ -5550,6 +5698,7 @@ class Graph():
5550
5698
  def _adamic_adar(self):
5551
5699
  """Lazily define and cache the self._adamic_adar relationship."""
5552
5700
  _adamic_adar_rel = self._model.Relationship(f"{{node_u:{self._NodeConceptStr}}} and {{node_v:{self._NodeConceptStr}}} have adamic adar score {{score:Float}}")
5701
+ _adamic_adar_rel.annotate(annotations.track("graphs", "adamic_adar"))
5553
5702
 
5554
5703
  node_u, node_v, common_neighbor = self.Node.ref(), self.Node.ref(), self.Node.ref()
5555
5704
  neighbor_count = Integer.ref()
@@ -5648,6 +5797,7 @@ class Graph():
5648
5797
  def _preferential_attachment(self):
5649
5798
  """Lazily define and cache the self._preferential_attachment relationship."""
5650
5799
  _preferential_attachment_rel = self._model.Relationship(f"{{node_u:{self._NodeConceptStr}}} and {{node_v:{self._NodeConceptStr}}} have preferential attachment score {{score:Integer}}")
5800
+ _preferential_attachment_rel.annotate(annotations.track("graphs", "preferential_attachment"))
5651
5801
 
5652
5802
  node_u, node_v = self.Node.ref(), self.Node.ref()
5653
5803
  count_u, count_v = Integer.ref(), Integer.ref()
@@ -2,6 +2,7 @@
2
2
  from __future__ import annotations
3
3
  from relationalai.semantics.metamodel import types, factory as f
4
4
  from relationalai.semantics.metamodel.util import OrderedSet
5
+ from relationalai.semantics.metamodel import builtins
5
6
 
6
7
  # Rel Annotations as IR Relations (to be used in IR Annotations)
7
8
 
@@ -28,7 +29,10 @@ inner_loop_non_stratified_annotation = f.annotation(inner_loop_non_stratified, [
28
29
 
29
30
  # collect all supported builtin rel annotations
30
31
  builtin_annotations = OrderedSet.from_iterable([
31
- arrow, no_diagnostics, no_inline, function, inner_loop, inner_loop_non_stratified
32
+ arrow, no_diagnostics, no_inline, function, inner_loop, inner_loop_non_stratified,
33
+ # track annotations on relations do not currently propagate into Rel
34
+ # TODO: from Thiago, ensure annotation goes from the Logical into the proper declaration
35
+ builtins.track,
32
36
  ])
33
37
 
34
38
  builtin_annotation_names = OrderedSet.from_iterable([a.name for a in builtin_annotations])