topologicpy 0.8.44__py3-none-any.whl → 0.8.46__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.
topologicpy/Graph.py CHANGED
@@ -4575,6 +4575,56 @@ class Graph:
4575
4575
  edges = [e for e in edges if Topology.IsInstance(e, "Edge")]
4576
4576
  return topologic.Graph.ByVerticesEdges(vertices, edges) # Hook to Core
4577
4577
 
4578
+ @staticmethod
4579
+ def Choice(graph, method: str = "vertex", weightKey="length", normalize: bool = False, nxCompatible: bool = False, key: str = "choice", colorKey="ch_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
4580
+ """
4581
+ This is an alias method for Graph.BetweenessCentrality. Returns the choice (Betweeness Centrality) of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
4582
+
4583
+ Parameters
4584
+ ----------
4585
+ graph : topologic_core.Graph
4586
+ The input graph.
4587
+ method : str , optional
4588
+ The method of computing the betweenness centrality. The options are "vertex" or "edge". The default is "vertex".
4589
+ weightKey : str , optional
4590
+ If specified, the value in the connected edges' dictionary specified by the weightKey string will be aggregated to calculate
4591
+ the shortest path. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead.
4592
+ This is used in weighted graphs. if weightKey is set to "Length" or "Distance", the length of the edge will be used as its weight.
4593
+ normalize : bool , optional
4594
+ If set to True, the values are normalized to be in the range 0 to 1. Otherwise they are not. The default is False.
4595
+ nxCompatible : bool , optional
4596
+ If set to True, and normalize input parameter is also set to True, the values are set to be identical to NetworkX values. Otherwise, they are normalized between 0 and 1. The default is False.
4597
+ key : str , optional
4598
+ The desired dictionary key under which to store the betweenness centrality score. The default is "betweenness_centrality".
4599
+ colorKey : str , optional
4600
+ The desired dictionary key under which to store the betweenness centrality color. The default is "betweenness_centrality".
4601
+ colorScale : str , optional
4602
+ The desired type of plotly color scales to use (e.g. "viridis", "plasma"). The default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
4603
+ In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
4604
+ mantissa : int , optional
4605
+ The desired length of the mantissa. The default is 6.
4606
+ tolerance : float , optional
4607
+ The desired tolerance. The default is 0.0001.
4608
+
4609
+ Returns
4610
+ -------
4611
+ list
4612
+ The choice (betweenness centrality) of the input list of vertices within the input graph. The values are in the range 0 to 1.
4613
+
4614
+ """
4615
+ return Graph.BetweennessCentrality(graph,
4616
+ method=method,
4617
+ weightKey=weightKey,
4618
+ normalize=normalize,
4619
+ nxCompatible=nxCompatible,
4620
+ key=key,
4621
+ colorKey=colorKey,
4622
+ colorScale=colorScale,
4623
+ mantissa=mantissa,
4624
+ tolerance=tolerance,
4625
+ silent=silent)
4626
+
4627
+
4578
4628
  @staticmethod
4579
4629
  def ChromaticNumber(graph, maxColors: int = 3, silent: bool = False):
4580
4630
  """
@@ -4750,7 +4800,8 @@ class Graph:
4750
4800
  weightJaccard: float = 0.0,
4751
4801
  vertexIDKey: str = "id",
4752
4802
  edgeWeightKey: str = None,
4753
- iterations: int = 3,
4803
+ vertexKey: str = None,
4804
+ iterations: int = 2,
4754
4805
  mantissa: int = 6,
4755
4806
  silent: bool = False):
4756
4807
  """
@@ -4797,8 +4848,10 @@ class Graph:
4797
4848
  The dictionary key under which to find the weight of the edge for weighted graphs.
4798
4849
  If this parameter is specified as "length" or "distance" then the length of the edge is used as its weight.
4799
4850
  The default is None which means all edges are treated as if they have a weight of 1.
4851
+ vertexKey: str , optional
4852
+ The vertex key to use for the Weifeiler-Lehman initial labels. The default is None which means it will use vertex degree as an initial label.
4800
4853
  iterations : int , optional
4801
- The desired number of Weisfeiler-Lehman iterations. Default is 3.
4854
+ The desired number of Weisfeiler-Lehman kernel iterations. Default is 2.
4802
4855
  mantissa : int , optional
4803
4856
  The desired length of the mantissa. The default is 6.
4804
4857
  silent : bool , optional
@@ -4885,7 +4938,7 @@ class Graph:
4885
4938
  If this parameter is specified as "length" or "distance" then the length of the edge is used as its weight.
4886
4939
  The default is None which means all edges are treated as if they have a weight of 1.
4887
4940
  iterations : int , optional
4888
- The desired number of Weisfeiler-Lehman iterations. Default is 3.
4941
+ The desired number of Weisfeiler-Lehman iterations. Default is 2.
4889
4942
  mantissa : int , optional
4890
4943
  The desired length of the mantissa. The default is 6.
4891
4944
 
@@ -5012,39 +5065,9 @@ class Graph:
5012
5065
 
5013
5066
  return round((vertex_score + edge_score) / 2, mantissa)
5014
5067
 
5015
- def weisfeiler_lehman_fingerprint(graph, iterations=3):
5016
- vertices = Graph.Vertices(graph)
5017
- labels = {}
5018
-
5019
- for v in vertices:
5020
- d = Topology.Dictionary(v)
5021
- label = str(Dictionary.ValueAtKey(d, "label")) if d and Dictionary.ValueAtKey(d, "label") else "0"
5022
- labels[v] = label
5023
-
5024
- all_label_counts = Counter()
5025
-
5026
- for _ in range(iterations):
5027
- new_labels = {}
5028
- for v in vertices:
5029
- neighbors = Graph.AdjacentVertices(graph, v)
5030
- neighbor_labels = sorted(labels.get(n, "0") for n in neighbors)
5031
- long_label = labels[v] + "_" + "_".join(neighbor_labels)
5032
- hashed_label = hashlib.md5(long_label.encode()).hexdigest()
5033
- new_labels[v] = hashed_label
5034
- all_label_counts[hashed_label] += 1
5035
- labels = new_labels
5036
-
5037
- return all_label_counts
5038
-
5039
- def weisfeiler_lehman_similarity(graphA, graphB, iterations=3, mantissa=6):
5040
- f1 = weisfeiler_lehman_fingerprint(graphA, iterations)
5041
- f2 = weisfeiler_lehman_fingerprint(graphB, iterations)
5042
-
5043
- common_labels = set(f1.keys()) & set(f2.keys())
5044
- score = sum(min(f1[label], f2[label]) for label in common_labels)
5045
- norm = max(sum(f1.values()), sum(f2.values()), 1)
5046
-
5047
- return round(score / norm, mantissa)
5068
+ def weisfeiler_lehman_similarity(graphA, graphB, key=None, iterations=3, mantissa=6):
5069
+ score = Graph.WLKernel(graphA, graphB, key=key, iterations=iterations, normalize=True, mantissa=mantissa)
5070
+ return score
5048
5071
 
5049
5072
  if not Topology.IsInstance(graphA, "graph"):
5050
5073
  if not silent:
@@ -5080,7 +5103,7 @@ class Graph:
5080
5103
  jaccard_score = weighted_jaccard_similarity(graphA, graphB, vertexIDKey=vertexIDKey, edgeWeightKey=edgeWeightKey, mantissa=mantissa) if weightJaccard else 0
5081
5104
  pagerank_score = pagerank_similarity(graphA, graphB, mantissa=mantissa) if weightPageRank else 0
5082
5105
  structure_score = structure_similarity(graphA, graphB, mantissa=mantissa) if weightStructure else 0
5083
- weisfeiler_lehman_score = weisfeiler_lehman_similarity(graphA, graphB, iterations, mantissa=mantissa) if weightWeisfeilerLehman else 0
5106
+ weisfeiler_lehman_score = weisfeiler_lehman_similarity(graphA, graphB, key=vertexKey, iterations=iterations, mantissa=mantissa) if weightWeisfeilerLehman else 0
5084
5107
 
5085
5108
  weighted_sum = (
5086
5109
  accessibility_centrality_score * weightAccessibilityCentrality +
@@ -5427,94 +5450,159 @@ class Graph:
5427
5450
  graph = Graph.RemoveVertex(graph,ev, tolerance=tolerance)
5428
5451
  return graph
5429
5452
 
5453
+
5430
5454
  @staticmethod
5431
- def ClosenessCentrality(graph, weightKey="length", normalize: bool = False, nxCompatible: bool = True, key: str = "closeness_centrality", colorKey="cc_color", colorScale="viridis", mantissa: int = 6, tolerance: float = 0.001, silent: bool = False):
5455
+ def ClosenessCentrality(
5456
+ graph,
5457
+ weightKey: str = "length",
5458
+ normalize: bool = False,
5459
+ nxCompatible: bool = True,
5460
+ key: str = "closeness_centrality",
5461
+ colorKey: str = "cc_color",
5462
+ colorScale: str = "viridis",
5463
+ mantissa: int = 6,
5464
+ tolerance: float = 0.0001,
5465
+ silent: bool = False
5466
+ ):
5432
5467
  """
5433
- Returns the closeness centrality of the input graph. The order of the returned list is the same as the order of vertices/edges. See https://en.wikipedia.org/wiki/Betweenness_centrality.
5468
+ Returns the closeness centrality of the input graph. The order of the returned
5469
+ list matches the order of Graph.Vertices(graph).
5470
+ See: https://en.wikipedia.org/wiki/Closeness_centrality
5434
5471
 
5435
5472
  Parameters
5436
5473
  ----------
5437
5474
  graph : topologic_core.Graph
5438
5475
  The input graph.
5439
5476
  weightKey : str , optional
5440
- If specified, the value in the connected edges' dictionary specified by the weightKey string will be aggregated to calculate
5441
- the shortest path. If a numeric value cannot be retrieved from an edge, a value of 1 is used instead.
5442
- This is used in weighted graphs. if weightKey is set to "Length" or "Distance", the length of the edge will be used as its weight.
5477
+ If specified, this edge attribute will be used as the distance weight when
5478
+ computing shortest paths. If set to a name containing "Length" or "Distance",
5479
+ it will be mapped to "length".
5480
+ Note: Graph.NetworkXGraph automatically provides a "length" attribute on all edges.
5443
5481
  normalize : bool , optional
5444
- If set to True, the values are normalized to be in the range 0 to 1. Otherwise they are not. The default is False.
5482
+ If True, the returned values are rescaled to [0, 1]. Otherwise raw values
5483
+ from NetworkX (optionally using the improved formula) are returned.
5445
5484
  nxCompatible : bool , optional
5446
- If set to True, use networkX to scale by the fraction of nodes reachable. This gives the Wasserman and Faust improved formula.
5447
- For single component graphs it is the same as the original formula.
5485
+ If True, use NetworkX's wf_improved scaling (Wasserman and Faust).
5486
+ For single-component graphs it matches the original formula.
5448
5487
  key : str , optional
5449
- The desired dictionary key under which to store the closeness centrality score. The default is "closeness_centrality".
5488
+ The dictionary key under which to store the closeness centrality score.
5450
5489
  colorKey : str , optional
5451
- The desired dictionary key under which to store the closeness centrality color. The default is "cc_color".
5490
+ The dictionary key under which to store a color derived from the score.
5452
5491
  colorScale : str , optional
5453
- The desired type of plotly color scales to use (e.g. "viridis", "plasma"). The default is "viridis". For a full list of names, see https://plotly.com/python/builtin-colorscales/.
5454
- In addition to these, three color-blind friendly scales are included. These are "protanopia", "deuteranopia", and "tritanopia" for red, green, and blue colorblindness respectively.
5492
+ Plotly color scale name (e.g., "viridis", "plasma").
5455
5493
  mantissa : int , optional
5456
5494
  The desired length of the mantissa. The default is 6.
5457
5495
  tolerance : float , optional
5458
5496
  The desired tolerance. The default is 0.0001.
5497
+ silent : bool , optional
5498
+ If set to True, error and warning messages are suppressed. The default is False.
5459
5499
 
5460
5500
  Returns
5461
5501
  -------
5462
- list
5463
- The closeness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
5464
-
5502
+ list[float]
5503
+ Closeness centrality values for vertices in the same order as Graph.Vertices(graph).
5465
5504
  """
5466
5505
  import warnings
5467
-
5468
5506
  try:
5469
5507
  import networkx as nx
5470
- except:
5471
- print("Graph.ClosenessCentrality - Information: Installing required networkx library.")
5472
- try:
5473
- os.system("pip install networkx")
5474
- except:
5475
- os.system("pip install networkx --user")
5476
- try:
5477
- import networkx as nx
5478
- print("Graph.ClosenessCentrality - Infromation: networkx library installed correctly.")
5479
- except:
5480
- warnings.warn("Graph.ClosenessCentrality - Error: Could not import networkx. Please try to install networkx manually. Returning None.")
5481
- return None
5482
-
5508
+ except Exception as e:
5509
+ warnings.warn(
5510
+ f"Graph.ClosenessCentrality - Error: networkx is required but not installed ({e}). Returning None."
5511
+ )
5512
+ return None
5513
+
5483
5514
  from topologicpy.Dictionary import Dictionary
5484
5515
  from topologicpy.Color import Color
5485
5516
  from topologicpy.Topology import Topology
5486
5517
  from topologicpy.Helper import Helper
5487
5518
 
5519
+ # Topology.IsInstance is case-insensitive, so a single call is sufficient.
5488
5520
  if not Topology.IsInstance(graph, "graph"):
5489
5521
  if not silent:
5490
- print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.")
5522
+ print("Graph.ClosenessCentrality - Error: The input is not a valid Graph. Returning None.")
5491
5523
  return None
5492
-
5493
- if weightKey:
5494
- if "len" in weightKey.lower() or "dis" in weightKey.lower():
5524
+ vertices = Graph.Vertices(graph)
5525
+ if len(vertices) == 0:
5526
+ if not silent:
5527
+ print("Graph.ClosenessCentrality - Warning: Graph has no vertices. Returning [].")
5528
+ return []
5529
+
5530
+ # Normalize the weight key semantics
5531
+ distance_attr = None
5532
+ if isinstance(weightKey, str) and weightKey:
5533
+ if ("len" in weightKey.lower()) or ("dis" in weightKey.lower()):
5495
5534
  weightKey = "length"
5535
+ distance_attr = weightKey
5536
+
5537
+ # Build the NX graph
5496
5538
  nx_graph = Graph.NetworkXGraph(graph)
5497
- vertices = Graph.Vertices(graph)
5498
- vertices_dict = nx.closeness_centrality(nx_graph, distance=weightKey, wf_improved=nxCompatible)
5499
- values = [round(v, mantissa) for v in list(vertices_dict.values())]
5500
- if normalize == True:
5501
- if mantissa > 0: # We cannot round numbers from 0 to 1 with a mantissa = 0.
5502
- values = [round(v, mantissa) for v in Helper.Normalize(values)]
5539
+
5540
+ # Graph.NetworkXGraph automatically adds "length" to all edges.
5541
+ # So if distance_attr == "length", we trust it and skip per-edge checks.
5542
+ if distance_attr and distance_attr != "length":
5543
+ # For any non-"length" custom attribute, verify presence; else fall back unweighted.
5544
+ attr_missing = any(
5545
+ (distance_attr not in data) or (data[distance_attr] is None)
5546
+ for _, _, data in nx_graph.edges(data=True)
5547
+ )
5548
+ if attr_missing:
5549
+ if not silent:
5550
+ print("Graph.ClosenessCentrality - Warning: The specified edge attribute was not found on all edges. Falling back to unweighted closeness.")
5551
+ distance_arg = None
5503
5552
  else:
5504
- values = Helper.Normalize(values)
5505
- min_value = 0
5506
- max_value = 1
5553
+ distance_arg = distance_attr
5507
5554
  else:
5508
- min_value = min(values)
5509
- max_value = max(values)
5555
+ # Use "length" directly or unweighted if distance_attr is falsy.
5556
+ distance_arg = distance_attr if distance_attr else None
5510
5557
 
5511
- for i, value in enumerate(values):
5558
+ # Compute centrality (dict keyed by NetworkX nodes)
5559
+ try:
5560
+ cc_dict = nx.closeness_centrality(nx_graph, distance=distance_arg, wf_improved=nxCompatible)
5561
+ except Exception as e:
5562
+ if not silent:
5563
+ print(f"Graph.ClosenessCentrality - Error: NetworkX failed to compute centrality ({e}). Returning None.")
5564
+ return None
5565
+
5566
+ # NetworkX vertex ids are in the same numerice order as the list of vertices starting from 0.
5567
+ raw_values = []
5568
+ for i, v in enumerate(vertices):
5569
+ try:
5570
+ raw_values.append(float(cc_dict.get(i, 0.0)))
5571
+ except Exception:
5572
+ if not silent:
5573
+ print(f,"Graph.ClosenessCentrality - Warning: Could not retrieve score for vertex {i}. Assigning a Zero (0).")
5574
+ raw_values.append(0.0)
5575
+
5576
+ # Optional normalization ONLY once, then rounding once at the end
5577
+ values_for_return = Helper.Normalize(raw_values) if normalize else raw_values
5578
+
5579
+ # Values for color scaling should reflect the displayed numbers
5580
+ color_values = values_for_return
5581
+
5582
+ # Single rounding at the end for return values
5583
+ if mantissa is not None and mantissa >= 0:
5584
+ values_for_return = [round(v, mantissa) for v in values_for_return]
5585
+
5586
+ # Prepare color mapping range, guarding equal-range case
5587
+ if color_values:
5588
+ min_value = min(color_values)
5589
+ max_value = max(color_values)
5590
+ else:
5591
+ min_value, max_value = 0.0, 1.0
5592
+
5593
+ if abs(max_value - min_value) < tolerance:
5594
+ max_value = min_value + tolerance
5595
+
5596
+ # Annotate vertices with score and color
5597
+ for i, value in enumerate(color_values):
5512
5598
  d = Topology.Dictionary(vertices[i])
5513
- color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
5514
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
5599
+ color_hex = Color.AnyToHex(
5600
+ Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale)
5601
+ )
5602
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [values_for_return[i], color_hex])
5515
5603
  vertices[i] = Topology.SetDictionary(vertices[i], d)
5516
5604
 
5517
- return values
5605
+ return values_for_return
5518
5606
 
5519
5607
  @staticmethod
5520
5608
  def Community(graph, key: str = "partition", mantissa: int = 6, tolerance: float = 0.0001, silent: bool = False):
@@ -5684,7 +5772,7 @@ class Graph:
5684
5772
  @staticmethod
5685
5773
  def Connectivity(graph, vertices=None, weightKey: str = None, normalize: bool = False, key: str = "connectivity", colorKey: str = "cn_color", colorScale="Viridis", mantissa: int = 6, tolerance = 0.0001, silent = False):
5686
5774
  """
5687
- Return the connectivity measure of the input list of vertices within the input graph. The order of the returned list is the same as the order of the input list of vertices. If no vertices are specified, the connectivity of all the vertices in the input graph is computed. See https://www.spacesyntax.online/term/connectivity/.
5775
+ This is an alias method for Graph.DegreeCentrality. Return the connectivity measure of the input list of vertices within the input graph. The order of the returned list is the same as the order of the input list of vertices. If no vertices are specified, the connectivity of all the vertices in the input graph is computed. See https://www.spacesyntax.online/term/connectivity/.
5688
5776
 
5689
5777
  Parameters
5690
5778
  ----------
@@ -5718,38 +5806,17 @@ class Graph:
5718
5806
  The connectivity score of the input list of vertices within the input graph. The values are in the range 0 to 1 if normalized.
5719
5807
 
5720
5808
  """
5721
-
5722
- from topologicpy.Topology import Topology
5723
- from topologicpy.Dictionary import Dictionary
5724
- from topologicpy.Helper import Helper
5725
- from topologicpy.Color import Color
5726
-
5727
- if not Topology.IsInstance(graph, "Graph"):
5728
- if not silent:
5729
- print("Graph.Connectivity - Error: The input graph is not a valid graph. Returning None.")
5730
- return None
5731
- if vertices == None:
5732
- vertices = Graph.Vertices(graph)
5733
- values = [Graph.VertexDegree(graph, v, weightKey=weightKey, mantissa=mantissa, tolerance=tolerance, silent=silent) for v in vertices]
5734
- values = [round(v, mantissa) for v in values]
5735
- if normalize == True:
5736
- if mantissa > 0:
5737
- values = [round(v, mantissa) for v in Helper.Normalize(values)]
5738
- else:
5739
- values = Helper.Normalize(values)
5740
- min_value = 0
5741
- max_value = 1
5742
- else:
5743
- min_value = min(values)
5744
- max_value = max(values)
5809
+ return Graph.DegreeCentrality(graph=graph,
5810
+ vertices=vertices,
5811
+ weightKey=weightKey,
5812
+ normalize=normalize,
5813
+ key="connectivity",
5814
+ colorKey=colorKey,
5815
+ colorScale=colorScale,
5816
+ mantissa=mantissa,
5817
+ tolerance=tolerance,
5818
+ silent=silent)
5745
5819
 
5746
- for i, value in enumerate(values):
5747
- color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
5748
- d = Topology.Dictionary(vertices[i])
5749
- d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
5750
- v = Topology.SetDictionary(vertices[i], d)
5751
- return values
5752
-
5753
5820
  @staticmethod
5754
5821
  def ContainsEdge(graph, edge, tolerance=0.0001):
5755
5822
  """
@@ -5884,7 +5951,7 @@ class Graph:
5884
5951
  tolerance: float = 0.001,
5885
5952
  silent: bool = False):
5886
5953
  """
5887
- Returns the degree centrality of the input graph. The order of the returned list is the same as the order of vertices. See https://en.wikipedia.org/wiki/Degree_centrality.
5954
+ Returns the degree centrality of the input graph. The order of the returned list is the same as the order of vertices. See https://en.wikipedia.org/wiki/Degree_centrality.
5888
5955
 
5889
5956
  Parameters
5890
5957
  ----------
@@ -5911,30 +5978,42 @@ class Graph:
5911
5978
  Returns
5912
5979
  -------
5913
5980
  list
5914
- The betweenness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
5981
+ The degree centrality of the input list of vertices within the input graph. The values are in the range 0 to 1.
5915
5982
 
5916
5983
  """
5917
5984
 
5918
- from topologicpy.Dictionary import Dictionary
5919
- from topologicpy.Color import Color
5920
5985
  from topologicpy.Topology import Topology
5986
+ from topologicpy.Dictionary import Dictionary
5921
5987
  from topologicpy.Helper import Helper
5988
+ from topologicpy.Color import Color
5922
5989
 
5923
- if not Topology.IsInstance(graph, "graph"):
5990
+ if not Topology.IsInstance(graph, "Graph"):
5924
5991
  if not silent:
5925
5992
  print("Graph.DegreeCentrality - Error: The input graph is not a valid graph. Returning None.")
5926
5993
  return None
5994
+ if vertices == None:
5995
+ vertices = Graph.Vertices(graph)
5996
+ values = [Graph.VertexDegree(graph, v, weightKey=weightKey, mantissa=mantissa, tolerance=tolerance, silent=silent) for v in vertices]
5997
+ if normalize == True:
5998
+ if mantissa > 0:
5999
+ values = [round(v, mantissa) for v in Helper.Normalize(values)]
6000
+ else:
6001
+ values = Helper.Normalize(values)
6002
+ min_value = 0
6003
+ max_value = 1
6004
+ else:
6005
+ min_value = min(values)
6006
+ max_value = max(values)
6007
+
6008
+ if abs(max_value - min_value) < tolerance:
6009
+ max_value = min_value + tolerance
5927
6010
 
5928
- return Graph.Connectivity(graph,
5929
- vertices=vertices,
5930
- weightKey=weightKey,
5931
- normalize=normalize,
5932
- key=key,
5933
- colorKey=colorKey,
5934
- colorScale=colorScale,
5935
- mantissa=mantissa,
5936
- tolerance=tolerance,
5937
- silent=silent)
6011
+ for i, value in enumerate(values):
6012
+ color = Color.AnyToHex(Color.ByValueInRange(value, minValue=min_value, maxValue=max_value, colorScale=colorScale))
6013
+ d = Topology.Dictionary(vertices[i])
6014
+ d = Dictionary.SetValuesAtKeys(d, [key, colorKey], [value, color])
6015
+ v = Topology.SetDictionary(vertices[i], d)
6016
+ return values
5938
6017
 
5939
6018
  @staticmethod
5940
6019
  def DegreeMatrix(graph):
@@ -9442,6 +9521,70 @@ class Graph:
9442
9521
  incoming_vertices.append(Graph.NearestVertex(graph, sv))
9443
9522
  return incoming_vertices
9444
9523
 
9524
+ @staticmethod
9525
+ def Integration(
9526
+ graph,
9527
+ weightKey: str = "length",
9528
+ normalize: bool = False,
9529
+ nxCompatible: bool = True,
9530
+ key: str = "integration",
9531
+ colorKey: str = "in_color",
9532
+ colorScale: str = "viridis",
9533
+ mantissa: int = 6,
9534
+ tolerance: float = 0.0001,
9535
+ silent: bool = False
9536
+ ):
9537
+ """
9538
+ This is an alias method for Graph.ClosenessCentrality. Returns the integration (closeness centrality) of the input graph. The order of the returned
9539
+ list matches the order of Graph.Vertices(graph).
9540
+ See: https://en.wikipedia.org/wiki/Closeness_centrality
9541
+
9542
+ Parameters
9543
+ ----------
9544
+ graph : topologic_core.Graph
9545
+ The input graph.
9546
+ weightKey : str , optional
9547
+ If specified, this edge attribute will be used as the distance weight when
9548
+ computing shortest paths. If set to a name containing "Length" or "Distance",
9549
+ it will be mapped to "length".
9550
+ Note: Graph.NetworkXGraph automatically provides a "length" attribute on all edges.
9551
+ normalize : bool , optional
9552
+ If True, the returned values are rescaled to [0, 1]. Otherwise raw values
9553
+ from NetworkX (optionally using the improved formula) are returned.
9554
+ nxCompatible : bool , optional
9555
+ If True, use NetworkX's wf_improved scaling (Wasserman and Faust).
9556
+ For single-component graphs it matches the original formula.
9557
+ key : str , optional
9558
+ The dictionary key under which to store the closeness centrality score.
9559
+ colorKey : str , optional
9560
+ The dictionary key under which to store a color derived from the score.
9561
+ colorScale : str , optional
9562
+ Plotly color scale name (e.g., "viridis", "plasma").
9563
+ mantissa : int , optional
9564
+ The desired length of the mantissa. The default is 6.
9565
+ tolerance : float , optional
9566
+ The desired tolerance. The default is 0.0001.
9567
+ silent : bool , optional
9568
+ If set to True, error and warning messages are suppressed. The default is False.
9569
+
9570
+ Returns
9571
+ -------
9572
+ list[float]
9573
+ Integration (closeness centrality) values for vertices in the same order as Graph.Vertices(graph).
9574
+ """
9575
+ return Graph.ClosenessCentrality(
9576
+ graph,
9577
+ weightKey=weightKey,
9578
+ normalize=normalize,
9579
+ nxCompatible=nxCompatible,
9580
+ key=key,
9581
+ colorKey=colorKey,
9582
+ colorScale=colorScale,
9583
+ mantissa=mantissa,
9584
+ tolerance=tolerance,
9585
+ silent=silent
9586
+ )
9587
+
9445
9588
  @staticmethod
9446
9589
  def IsBipartite(graph, tolerance=0.0001):
9447
9590
  """
@@ -13749,6 +13892,171 @@ class Graph:
13749
13892
  diffBA = Graph.Difference(graphB, graphA, vertexKeys=vertexKeys, useCentroid=useCentroid, tolerance=tolerance, silent=True)
13750
13893
  return Graph.Union(diffAB, diffBA, vertexKeys=vertexKeys, useCentroid=useCentroid, tolerance=tolerance, silent=True)
13751
13894
 
13895
+ @staticmethod
13896
+ def WLFeatures(graph, key: str = None, iterations: int = 2, silent: bool = False):
13897
+ """
13898
+ Returns a Weisfeiler-Lehman subtree features for a Graph. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
13899
+
13900
+ Parameters
13901
+ ----------
13902
+ graph : topologic_core.Graph
13903
+ The input graph.
13904
+ key : str , optional
13905
+ The vertex key to use as an initial label. The default is None which means the vertex degree is used instead.
13906
+ iterations : int , optional
13907
+ The desired number of WL iterations. (non-negative int). The default is 2.
13908
+ silent : bool, optional
13909
+ If set to True, error and warning messages are suppressed. Default is False.
13910
+
13911
+ Returns
13912
+ -------
13913
+ dict
13914
+ {feature_id: count} where feature_id is an int representing a WL label.
13915
+ """
13916
+
13917
+ from topologicpy.Topology import Topology
13918
+ from topologicpy.Dictionary import Dictionary
13919
+ from collections import defaultdict
13920
+
13921
+ def _neighbors_map(graph):
13922
+ """
13923
+ Returns:
13924
+ vertices: list of vertex objects in a stable order
13925
+ vidx: dict mapping vertex -> index
13926
+ nbrs: dict index -> sorted list of neighbor indices
13927
+ """
13928
+ vertices = Graph.Vertices(graph)
13929
+ vidx = {v: i for i, v in enumerate(vertices)}
13930
+ nbrs = {}
13931
+ for v in vertices:
13932
+ i = vidx[v]
13933
+ adj = Graph.AdjacentVertices(graph, v) or []
13934
+ nbrs[i] = sorted(vidx[a] for a in adj if a in vidx and a is not v)
13935
+ return vertices, vidx, nbrs
13936
+
13937
+ def _initial_labels(graph, key=None, default="degree"):
13938
+ """
13939
+ Returns an integer label per node index using either vertex dictionary labels
13940
+ or a structural default (degree or constant).
13941
+ """
13942
+ vertices, vidx, nbrs = _neighbors_map(graph)
13943
+ labels = {}
13944
+ if key:
13945
+ found_any = False
13946
+ tmp = {}
13947
+ for v in vertices:
13948
+ d = Topology.Dictionary(v)
13949
+ val = Dictionary.ValueAtKey(d, key)
13950
+ if val is not None:
13951
+ found_any = True
13952
+ tmp[vidx[v]] = str(val) if val is not None else None
13953
+ if found_any:
13954
+ # fill missing with a sentinel
13955
+ for i, val in tmp.items():
13956
+ labels[i] = val if val is not None else "__MISSING__"
13957
+ else:
13958
+ # fall back to structural init if no labels exist
13959
+ if default == "degree":
13960
+ labels = {i: str(len(nbrs[i])) for i in nbrs}
13961
+ else:
13962
+ labels = {i: "0" for i in nbrs}
13963
+ else: # Add a vertex degree information.
13964
+ _ = Graph.DegreeCentrality(graph, key="_dc_")
13965
+ return _initial_labels(graph, key="_dc_")
13966
+ return labels, nbrs
13967
+
13968
+ def _canonize_string_labels(str_labels):
13969
+ """
13970
+ Deterministically map arbitrary strings to dense integer ids.
13971
+ Returns:
13972
+ int_labels: dict node_index -> int label
13973
+ vocab: dict string_label -> int id
13974
+ """
13975
+ # stable order by string to keep mapping deterministic across runs
13976
+ unique = sorted(set(str_labels.values()))
13977
+ vocab = {lab: k for k, lab in enumerate(unique)}
13978
+ return {i: vocab[s] for i, s in str_labels.items()}, vocab
13979
+
13980
+ from topologicpy.Topology import Topology
13981
+
13982
+ if not Topology.IsInstance(graph, "Graph"):
13983
+ if not silent:
13984
+ print("Graph.WLFeatures - Error: The input graph parameter is not a valid topologic graph. Returning None.")
13985
+ return None
13986
+
13987
+ str_labels, nbrs = _initial_labels(graph, key=key)
13988
+ features = defaultdict(int)
13989
+
13990
+ # iteration 0
13991
+ labels, _ = _canonize_string_labels(str_labels)
13992
+ for lab in labels.values():
13993
+ features[lab] += 1
13994
+
13995
+ # WL iterations
13996
+ cur = labels
13997
+ for _ in range(iterations):
13998
+ new_str = {}
13999
+ for i in nbrs:
14000
+ neigh = [cur[j] for j in nbrs[i]]
14001
+ neigh.sort()
14002
+ new_str[i] = f"{cur[i]}|{','.join(map(str, neigh))}"
14003
+ cur, _ = _canonize_string_labels(new_str)
14004
+ for lab in cur.values():
14005
+ features[lab] += 1
14006
+
14007
+ return dict(features)
14008
+
14009
+ @staticmethod
14010
+ def WLKernel(graphA, graphB, key: str = None, iterations: int = 2, normalize: bool = True, mantissa: int = 6, silent: bool = False):
14011
+ """
14012
+ Returns a cosine-normalized Weisfeiler-Lehman kernel between two graphs. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
14013
+
14014
+ Parameters
14015
+ ----------
14016
+ graphA : topologic_core.Graph
14017
+ The first input graph.
14018
+ graphB : topologic_core.Graph
14019
+ The second input graph.
14020
+ key : str , optional
14021
+ The vertex key to use as an initial label. The default is None which means the vertex degree is used instead.
14022
+ iterations : int , optional
14023
+ The desired number of WL iterations. (non-negative int). The default is 2.
14024
+ normalize : bool , optional
14025
+ if set to True, the returned value is normalized between 0 and 1. The default is True.
14026
+ mantissa : int , optional
14027
+ The desired length of the mantissa. The default is 6.
14028
+
14029
+ Returns
14030
+ -------
14031
+ float
14032
+ The cosine-normalized Weisfeiler-Lehman kernel
14033
+ """
14034
+ from topologicpy.Topology import Topology
14035
+
14036
+ if not Topology.IsInstance(graphA, "Graph"):
14037
+ if not silent:
14038
+ print("Graph.WLFeatures - Error: The input graphA parameter is not a valid topologic graph. Returning None.")
14039
+ return None
14040
+ if not Topology.IsInstance(graphB, "Graph"):
14041
+ if not silent:
14042
+ print("Graph.WLFeatures - Error: The input graphB parameter is not a valid topologic graph. Returning None.")
14043
+ return None
14044
+ f1 = Graph.WLFeatures(graphA, key=key, iterations=iterations)
14045
+ f2 = Graph.WLFeatures(graphB, key=key, iterations=iterations)
14046
+
14047
+ # dot product
14048
+ keys = set(f1) | set(f2)
14049
+ dot = sum(f1.get(k, 0) * f2.get(k, 0) for k in keys)
14050
+
14051
+ if not normalize:
14052
+ return round(float(dot), mantissa)
14053
+
14054
+ import math
14055
+ n1 = math.sqrt(sum(v*v for v in f1.values()))
14056
+ n2 = math.sqrt(sum(v*v for v in f2.values()))
14057
+ return_value = float(dot) / (n1 * n2) if n1 > 0 and n2 > 0 else 0.0
14058
+ return round(return_value, mantissa)
14059
+
13752
14060
  @staticmethod
13753
14061
  def XOR(graphA, graphB, vertexKeys, useCentroid: bool = False, tolerance: float = 0.001, silent: bool = False):
13754
14062
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.8.44'
1
+ __version__ = '0.8.46'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.44
3
+ Version: 0.8.46
4
4
  Summary: An AI-Powered Spatial Modelling and Analysis Software Library for Architecture, Engineering, and Construction.
5
5
  Author-email: Wassim Jabi <wassim.jabi@gmail.com>
6
6
  License: AGPL v3 License
@@ -12,7 +12,7 @@ topologicpy/Dictionary.py,sha256=sPskW5bopbDzLz6MGKm8lN_OeyeAgsqdLvwwNcG0J3g,446
12
12
  topologicpy/Edge.py,sha256=dLoAPuRKbjVg_dzloTgjRnQyv_05U9nfrtLO3tqyuys,74167
13
13
  topologicpy/EnergyModel.py,sha256=Pyb28gDDwhzlQIH0xqAygqS0P3SJxWyyV7OWS_AAfRs,53856
14
14
  topologicpy/Face.py,sha256=pN1fssyDLYWf1vU0NOBRx69DaUL958wRSxT-7VBCuCg,203184
15
- topologicpy/Graph.py,sha256=GykBbB8KKcy2oPV2Dsnz_vkyWGgczxLi0puVx5NJUYU,648088
15
+ topologicpy/Graph.py,sha256=io5l8IV60q5ySrpPTid7TL0he4ouD9nJIoTU-erL6Z8,661704
16
16
  topologicpy/Grid.py,sha256=EbI2NcYhQDpD5mItd7A1Lpr8Puuf87vZPWuoh7_gChQ,18483
17
17
  topologicpy/Helper.py,sha256=qEsE4yaboEGW94q9lFCff0I_JwwTTQnDAFXw006yHaQ,31203
18
18
  topologicpy/Honeybee.py,sha256=yctkwfdupKnp7bAOjP1Z4YaYpRrWoMEb4gz9Z5zaWwE,21751
@@ -30,9 +30,9 @@ topologicpy/Vector.py,sha256=X12eqskn28bdB7sLY1EZhq3noPYzPbNEgHPb4a959ss,42302
30
30
  topologicpy/Vertex.py,sha256=RlGQnxQSb_kAus3tJgXd-v-Ptubtt09PQPA9IMwfXmI,84835
31
31
  topologicpy/Wire.py,sha256=sJE8qwqYOomvN3snMWmj2P2-Sq25ul_OQ95YFz6DFUw,230553
32
32
  topologicpy/__init__.py,sha256=RMftibjgAnHB1vdL-muo71RwMS4972JCxHuRHOlU428,928
33
- topologicpy/version.py,sha256=H1GigrgdekxMys9lmJxIRnIFrNk4zrmX7xz5PrqPdxE,23
34
- topologicpy-0.8.44.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
35
- topologicpy-0.8.44.dist-info/METADATA,sha256=YU_ze05mFUZrce4XzGuVddWEEOzOM_oTPndhJHejKC0,10535
36
- topologicpy-0.8.44.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- topologicpy-0.8.44.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
38
- topologicpy-0.8.44.dist-info/RECORD,,
33
+ topologicpy/version.py,sha256=lWn332GY1e40h1ASpBjJjwG1eoQEgys8nvUCuPd_VMU,23
34
+ topologicpy-0.8.46.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
35
+ topologicpy-0.8.46.dist-info/METADATA,sha256=TJ0qyM1tswkGy-aJjUmdi5Zmn1BLH1RAoFV9iEi6EI8,10535
36
+ topologicpy-0.8.46.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
+ topologicpy-0.8.46.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
38
+ topologicpy-0.8.46.dist-info/RECORD,,