relationalai 0.11.3__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 (54) hide show
  1. relationalai/clients/config.py +7 -0
  2. relationalai/clients/direct_access_client.py +113 -0
  3. relationalai/clients/snowflake.py +41 -107
  4. relationalai/clients/use_index_poller.py +349 -188
  5. relationalai/early_access/dsl/bindings/csv.py +2 -2
  6. relationalai/early_access/metamodel/rewrite/__init__.py +5 -3
  7. relationalai/early_access/rel/rewrite/__init__.py +1 -1
  8. relationalai/errors.py +24 -3
  9. relationalai/semantics/internal/annotations.py +1 -0
  10. relationalai/semantics/internal/internal.py +22 -4
  11. relationalai/semantics/lqp/builtins.py +1 -0
  12. relationalai/semantics/lqp/executor.py +61 -12
  13. relationalai/semantics/lqp/intrinsics.py +23 -0
  14. relationalai/semantics/lqp/model2lqp.py +13 -4
  15. relationalai/semantics/lqp/passes.py +4 -6
  16. relationalai/semantics/lqp/primitives.py +12 -1
  17. relationalai/semantics/{rel → lqp}/rewrite/__init__.py +6 -0
  18. relationalai/semantics/lqp/rewrite/extract_common.py +362 -0
  19. relationalai/semantics/metamodel/builtins.py +20 -2
  20. relationalai/semantics/metamodel/factory.py +3 -2
  21. relationalai/semantics/metamodel/rewrite/__init__.py +3 -9
  22. relationalai/semantics/reasoners/graph/core.py +273 -71
  23. relationalai/semantics/reasoners/optimization/solvers_dev.py +20 -1
  24. relationalai/semantics/reasoners/optimization/solvers_pb.py +24 -3
  25. relationalai/semantics/rel/builtins.py +5 -1
  26. relationalai/semantics/rel/compiler.py +7 -19
  27. relationalai/semantics/rel/executor.py +2 -2
  28. relationalai/semantics/rel/rel.py +6 -0
  29. relationalai/semantics/rel/rel_utils.py +8 -1
  30. relationalai/semantics/sql/compiler.py +122 -42
  31. relationalai/semantics/sql/executor/duck_db.py +28 -3
  32. relationalai/semantics/sql/rewrite/denormalize.py +4 -6
  33. relationalai/semantics/sql/rewrite/recursive_union.py +23 -3
  34. relationalai/semantics/sql/sql.py +27 -0
  35. relationalai/semantics/std/__init__.py +2 -1
  36. relationalai/semantics/std/datetime.py +4 -0
  37. relationalai/semantics/std/re.py +83 -0
  38. relationalai/semantics/std/strings.py +1 -1
  39. relationalai/tools/cli.py +11 -4
  40. relationalai/tools/cli_controls.py +445 -60
  41. relationalai/util/format.py +78 -1
  42. {relationalai-0.11.3.dist-info → relationalai-0.12.0.dist-info}/METADATA +7 -5
  43. {relationalai-0.11.3.dist-info → relationalai-0.12.0.dist-info}/RECORD +51 -50
  44. relationalai/semantics/metamodel/rewrite/gc_nodes.py +0 -58
  45. relationalai/semantics/metamodel/rewrite/list_types.py +0 -109
  46. relationalai/semantics/rel/rewrite/extract_common.py +0 -451
  47. /relationalai/semantics/{rel → lqp}/rewrite/cdc.py +0 -0
  48. /relationalai/semantics/{metamodel → lqp}/rewrite/extract_keys.py +0 -0
  49. /relationalai/semantics/{metamodel → lqp}/rewrite/fd_constraints.py +0 -0
  50. /relationalai/semantics/{rel → lqp}/rewrite/quantify_vars.py +0 -0
  51. /relationalai/semantics/{metamodel → lqp}/rewrite/splinter.py +0 -0
  52. {relationalai-0.11.3.dist-info → relationalai-0.12.0.dist-info}/WHEEL +0 -0
  53. {relationalai-0.11.3.dist-info → relationalai-0.12.0.dist-info}/entry_points.txt +0 -0
  54. {relationalai-0.11.3.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()
@@ -3865,12 +3914,19 @@ class Graph():
3865
3914
 
3866
3915
 
3867
3916
  @include_in_docs
3868
- def triangle_count(self):
3917
+ def triangle_count(self, *, of: Optional[Relationship] = None):
3869
3918
  """Returns a binary relationship containing the number of unique triangles each node belongs to.
3870
3919
 
3871
3920
  A triangle is a set of three nodes where each node has a directed
3872
3921
  or undirected edge to the other two nodes, forming a 3-cycle.
3873
3922
 
3923
+ Parameters
3924
+ ----------
3925
+ of : Relationship, optional
3926
+ A unary relationship containing a subset of the graph's nodes. When
3927
+ provided, constrains the domain of the triangle count computation: only
3928
+ triangle counts of nodes in this relationship are computed and returned.
3929
+
3874
3930
  Returns
3875
3931
  -------
3876
3932
  Relationship
@@ -3926,6 +3982,31 @@ class Graph():
3926
3982
  3 4 0
3927
3983
  4 5 0
3928
3984
 
3985
+ >>> # 4. Use 'of' parameter to constrain the set of nodes to compute triangle counts of
3986
+ >>> # Define a subset containing only nodes 1 and 3
3987
+ >>> subset = model.Relationship(f"{{node:{Node}}} is in subset")
3988
+ >>> node = Node.ref()
3989
+ >>> where(union(node.id == 1, node.id == 3)).define(subset(node))
3990
+ >>>
3991
+ >>> # Get triangle counts only of nodes in the subset
3992
+ >>> constrained_triangle_count = graph.triangle_count(of=subset)
3993
+ >>> select(node.id, count).where(constrained_triangle_count(node, count)).inspect()
3994
+ ▰▰▰▰ Setup complete
3995
+ id count
3996
+ 0 1 1
3997
+ 1 3 1
3998
+
3999
+ Notes
4000
+ -----
4001
+ The ``triangle_count()`` method, called with no parameters, computes and caches
4002
+ the full triangle count relationship, providing efficient reuse across multiple
4003
+ calls to ``triangle_count()``. In contrast, ``triangle_count(of=subset)`` computes a
4004
+ constrained relationship specific to the passed-in ``subset`` and that
4005
+ call site. When a significant fraction of the triangle count relation is needed
4006
+ across a program, ``triangle_count()`` is typically more efficient; this is the
4007
+ typical case. Use ``triangle_count(of=subset)`` only when small subsets of the
4008
+ triangle count relationship are needed collectively across the program.
4009
+
3929
4010
  See Also
3930
4011
  --------
3931
4012
  triangle
@@ -3933,15 +4014,39 @@ class Graph():
3933
4014
  num_triangles
3934
4015
 
3935
4016
  """
4017
+ if of is not None:
4018
+ self._validate_node_subset_parameter(of)
4019
+ return self._triangle_count_of(of)
3936
4020
  return self._triangle_count
3937
4021
 
3938
4022
  @cached_property
3939
4023
  def _triangle_count(self):
3940
4024
  """Lazily define and cache the self._triangle_count relationship."""
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
4028
+
4029
+ def _triangle_count_of(self, nodes_subset: Relationship):
4030
+ """
4031
+ Create a triangle count relationship constrained to the subset of nodes
4032
+ in `nodes_subset`. Note this relationship is not cached; it is
4033
+ specific to the callsite.
4034
+ """
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
4038
+
4039
+ def _create_triangle_count_relationship(self, *, nodes_subset: Optional[Relationship]):
4040
+ """Create a triangle count relationship, optionally constrained to a subset of nodes."""
3941
4041
  _triangle_count_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} belongs to {{count:Integer}} triangles")
3942
4042
 
4043
+ if nodes_subset is None:
4044
+ node_constraint = self.Node # No constraint on nodes.
4045
+ else:
4046
+ node_constraint = nodes_subset(self.Node) # Nodes constrained to given subset.
4047
+
3943
4048
  where(
3944
- self.Node,
4049
+ node_constraint,
3945
4050
  _count := self._nonzero_triangle_count_fragment(self.Node) | 0
3946
4051
  ).define(_triangle_count_rel(self.Node, _count))
3947
4052
 
@@ -4061,7 +4166,7 @@ class Graph():
4061
4166
 
4062
4167
 
4063
4168
  @include_in_docs
4064
- def local_clustering_coefficient(self):
4169
+ def local_clustering_coefficient(self, *, of: Optional[Relationship] = None):
4065
4170
  """Returns a binary relationship containing the local clustering coefficient of each node.
4066
4171
 
4067
4172
  The local clustering coefficient quantifies how close a node's neighbors
@@ -4070,6 +4175,14 @@ class Graph():
4070
4175
  directly connecting them, and 1.0 indicates all neighbors have edges
4071
4176
  directly connecting them.
4072
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
+
4073
4186
  Returns
4074
4187
  -------
4075
4188
  Relationship
@@ -4096,17 +4209,6 @@ class Graph():
4096
4209
  | Directed | No | Undirected only. |
4097
4210
  | Weighted | Yes | Weights are ignored. |
4098
4211
 
4099
- Notes
4100
- -----
4101
- The formal definition of the local clustering coefficient (`C`) for a
4102
- node (`v`) can be given as::
4103
-
4104
- C(v) = (2 * num_edges) / (degree(v) * (degree(v) - 1))
4105
-
4106
- Here, `num_edges` represents the number of edges between the
4107
- neighbors of node `v`, and `degree(v)` represents the degree of the
4108
- node, i.e., the number of edges connected to the node.
4109
-
4110
4212
  Examples
4111
4213
  --------
4112
4214
  >>> from relationalai.semantics import Model, define, select, Float
@@ -4142,6 +4244,41 @@ class Graph():
4142
4244
  3 4 0.333333
4143
4245
  4 5 0.000000
4144
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
+
4145
4282
 
4146
4283
  See Also
4147
4284
  --------
@@ -4154,29 +4291,51 @@ class Graph():
4154
4291
  raise NotImplementedError(
4155
4292
  "`local_clustering_coefficient` is not applicable to directed graphs"
4156
4293
  )
4294
+
4295
+ if of is not None:
4296
+ self._validate_node_subset_parameter(of)
4297
+ return self._local_clustering_coefficient_of(of)
4157
4298
  return self._local_clustering_coefficient
4158
4299
 
4159
4300
  @cached_property
4160
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):
4161
4308
  """
4162
- Lazily define and cache the self._local_clustering_coefficient relationship,
4163
- 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.
4164
4312
  """
4165
- _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
4166
4316
 
4167
- if self.directed:
4168
- raise NotImplementedError(
4169
- "`local_clustering_coefficient is not defined for directed graphs."
4170
- )
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}}")
4171
4320
 
4172
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
+
4173
4332
  degree_no_self = Integer.ref()
4174
4333
  triangle_count = Integer.ref()
4175
4334
  where(
4176
- node,
4335
+ node_constraint,
4177
4336
  _lcc := where(
4178
- self._degree_no_self(node, degree_no_self),
4179
- self._triangle_count(node, triangle_count),
4337
+ degree_no_self_rel(node, degree_no_self),
4338
+ triangle_count_rel(node, triangle_count),
4180
4339
  degree_no_self > 1
4181
4340
  ).select(
4182
4341
  2.0 * triangle_count / (degree_no_self * (degree_no_self - 1.0))
@@ -4191,11 +4350,32 @@ class Graph():
4191
4350
  Lazily define and cache the self._degree_no_self relationship,
4192
4351
  a non-public helper for local_clustering_coefficient.
4193
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
+ """
4194
4368
  _degree_no_self_rel = self._model.Relationship(f"{{node:{self._NodeConceptStr}}} has degree excluding self loops {{num:Integer}}")
4195
4369
 
4196
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
+
4197
4377
  where(
4198
- self.Node(node),
4378
+ node_constraint,
4199
4379
  _dns := count(neighbor).per(node).where(self._no_loop_edge(node, neighbor)) | 0,
4200
4380
  ).define(_degree_no_self_rel(node, _dns))
4201
4381
 
@@ -4279,6 +4459,7 @@ class Graph():
4279
4459
  which only applies to undirected graphs.
4280
4460
  """
4281
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"))
4282
4463
 
4283
4464
  if self.directed:
4284
4465
  raise NotImplementedError(
@@ -4419,6 +4600,7 @@ class Graph():
4419
4600
  def _reachable_from(self):
4420
4601
  """Lazily define and cache the self._reachable_from relationship."""
4421
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"))
4422
4604
 
4423
4605
  node_a, node_b, node_c = self.Node.ref(), self.Node.ref(), self.Node.ref()
4424
4606
  define(_reachable_from_rel(node_a, node_a))
@@ -4561,9 +4743,12 @@ class Graph():
4561
4743
  def _distance(self):
4562
4744
  """Lazily define and cache the self._distance relationship."""
4563
4745
  if not self.weighted:
4564
- return self._distance_non_weighted
4746
+ _distance_rel = self._distance_non_weighted
4565
4747
  else:
4566
- return self._distance_weighted
4748
+ _distance_rel = self._distance_weighted
4749
+
4750
+ _distance_rel.annotate(annotations.track("graphs", "distance"))
4751
+ return _distance_rel
4567
4752
 
4568
4753
  @cached_property
4569
4754
  def _distance_weighted(self):
@@ -4689,6 +4874,7 @@ class Graph():
4689
4874
  def _weakly_connected_component(self):
4690
4875
  """Lazily define and cache the self._weakly_connected_component relationship."""
4691
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"))
4692
4878
 
4693
4879
  node, node_v, component = self.Node.ref(), self.Node.ref(), self.Node.ref()
4694
4880
  node, component = union(
@@ -4812,6 +4998,8 @@ class Graph():
4812
4998
  """
4813
4999
  _diameter_range_min_rel = self._model.Relationship("The graph has a min diameter range of {value:Integer}")
4814
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"))
4815
5003
 
4816
5004
  component_node_pairs = self._model.Relationship(f"component id {{cid:{self._NodeConceptStr}}} has node id {{nid:{self._NodeConceptStr}}}")
4817
5005
  nodeid, cid, degreevalue = self.Node.ref(), self.Node.ref(), Integer.ref()
@@ -4872,16 +5060,22 @@ class Graph():
4872
5060
 
4873
5061
  @include_in_docs
4874
5062
  def is_connected(self):
4875
- """Returns a query fragment that is satisfied if the graph is connected.
5063
+ """Returns a unary relationship containing whether the graph is connected.
4876
5064
 
4877
5065
  A graph is considered connected if every node is reachable from every
4878
5066
  other node in the underlying undirected graph.
4879
5067
 
4880
5068
  Returns
4881
5069
  -------
4882
- Fragment
4883
- A query fragment that can be used as a condition in other
4884
- 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.
4885
5079
 
4886
5080
  Supported Graph Types
4887
5081
  ---------------------
@@ -4899,8 +5093,6 @@ class Graph():
4899
5093
  --------
4900
5094
  **Connected Graph Example**
4901
5095
 
4902
- The following query will produce a result because the graph is connected.
4903
-
4904
5096
  >>> from relationalai.semantics import Model, define, select
4905
5097
  >>> from relationalai.semantics.reasoners.graph import Graph
4906
5098
  >>>
@@ -4918,17 +5110,14 @@ class Graph():
4918
5110
  ... Edge.new(src=n4, dst=n3),
4919
5111
  ... )
4920
5112
  >>>
4921
- >>> # 3. Use the fragment as a condition in a query
4922
- >>> select("Graph is connected").where(graph.is_connected()).inspect()
5113
+ >>> # 3. Select and inspect the relation
5114
+ >>> select(graph.is_connected()).inspect()
4923
5115
  ▰▰▰▰ Setup complete
4924
- v
4925
- 0 Graph is connected
5116
+ is_connected
5117
+ 0 True
4926
5118
 
4927
5119
  **Disconnected Graph Example**
4928
5120
 
4929
- The following query will produce no results because the graph is not
4930
- connected.
4931
-
4932
5121
  >>> from relationalai.semantics import Model, define, select
4933
5122
  >>> from relationalai.semantics.reasoners.graph import Graph
4934
5123
  >>>
@@ -4946,22 +5135,31 @@ class Graph():
4946
5135
  ... Edge.new(src=n4, dst=n5), # This edge creates a separate component
4947
5136
  ... )
4948
5137
  >>>
4949
- >>> # 3. The conditional query produces no output
4950
- >>> select("Graph is connected").where(graph.is_connected()).inspect()
5138
+ >>> # 3. Select and inspect the relation
5139
+ >>> select(graph.is_connected()).inspect()
4951
5140
  ▰▰▰▰ Setup complete
4952
- Empty DataFrame
4953
- Columns: []
4954
- Index: []
5141
+ is_connected
5142
+ 0 False
4955
5143
 
4956
5144
  """
4957
- # TODO (dba) This method is inconsistent with the other,
4958
- # public methods. It does not return a `Relationship`. Revisit
4959
- # this. See GH thread:
4960
- # https://github.com/RelationalAI/relationalai-python/pull/2077#discussion_r2190538074
4961
- 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(
4962
5154
  self._num_nodes(0) |
4963
5155
  count(self._reachable_from_min_node(self.Node.ref())) == self._num_nodes(Integer.ref())
4964
- )
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
4965
5163
 
4966
5164
 
4967
5165
  @include_in_docs
@@ -5127,6 +5325,7 @@ class Graph():
5127
5325
  def _jaccard_similarity(self):
5128
5326
  """Lazily define and cache the self._jaccard_similarity relationship."""
5129
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"))
5130
5329
 
5131
5330
  if not self.weighted:
5132
5331
  node_u, node_v = self.Node.ref(), self.Node.ref()
@@ -5378,6 +5577,7 @@ class Graph():
5378
5577
  def _cosine_similarity(self):
5379
5578
  """Lazily define and cache the self._cosine_similarity relationship."""
5380
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"))
5381
5581
 
5382
5582
  if not self.weighted:
5383
5583
  node_u, node_v = self.Node.ref(), self.Node.ref()
@@ -5498,6 +5698,7 @@ class Graph():
5498
5698
  def _adamic_adar(self):
5499
5699
  """Lazily define and cache the self._adamic_adar relationship."""
5500
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"))
5501
5702
 
5502
5703
  node_u, node_v, common_neighbor = self.Node.ref(), self.Node.ref(), self.Node.ref()
5503
5704
  neighbor_count = Integer.ref()
@@ -5596,6 +5797,7 @@ class Graph():
5596
5797
  def _preferential_attachment(self):
5597
5798
  """Lazily define and cache the self._preferential_attachment relationship."""
5598
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"))
5599
5801
 
5600
5802
  node_u, node_v = self.Node.ref(), self.Node.ref()
5601
5803
  count_u, count_v = Integer.ref(), Integer.ref()