topologicpy 0.8.45__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
@@ -4800,7 +4800,8 @@ class Graph:
4800
4800
  weightJaccard: float = 0.0,
4801
4801
  vertexIDKey: str = "id",
4802
4802
  edgeWeightKey: str = None,
4803
- iterations: int = 3,
4803
+ vertexKey: str = None,
4804
+ iterations: int = 2,
4804
4805
  mantissa: int = 6,
4805
4806
  silent: bool = False):
4806
4807
  """
@@ -4847,8 +4848,10 @@ class Graph:
4847
4848
  The dictionary key under which to find the weight of the edge for weighted graphs.
4848
4849
  If this parameter is specified as "length" or "distance" then the length of the edge is used as its weight.
4849
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.
4850
4853
  iterations : int , optional
4851
- The desired number of Weisfeiler-Lehman iterations. Default is 3.
4854
+ The desired number of Weisfeiler-Lehman kernel iterations. Default is 2.
4852
4855
  mantissa : int , optional
4853
4856
  The desired length of the mantissa. The default is 6.
4854
4857
  silent : bool , optional
@@ -4935,7 +4938,7 @@ class Graph:
4935
4938
  If this parameter is specified as "length" or "distance" then the length of the edge is used as its weight.
4936
4939
  The default is None which means all edges are treated as if they have a weight of 1.
4937
4940
  iterations : int , optional
4938
- The desired number of Weisfeiler-Lehman iterations. Default is 3.
4941
+ The desired number of Weisfeiler-Lehman iterations. Default is 2.
4939
4942
  mantissa : int , optional
4940
4943
  The desired length of the mantissa. The default is 6.
4941
4944
 
@@ -5062,39 +5065,9 @@ class Graph:
5062
5065
 
5063
5066
  return round((vertex_score + edge_score) / 2, mantissa)
5064
5067
 
5065
- def weisfeiler_lehman_fingerprint(graph, iterations=3):
5066
- vertices = Graph.Vertices(graph)
5067
- labels = {}
5068
-
5069
- for v in vertices:
5070
- d = Topology.Dictionary(v)
5071
- label = str(Dictionary.ValueAtKey(d, "label")) if d and Dictionary.ValueAtKey(d, "label") else "0"
5072
- labels[v] = label
5073
-
5074
- all_label_counts = Counter()
5075
-
5076
- for _ in range(iterations):
5077
- new_labels = {}
5078
- for v in vertices:
5079
- neighbors = Graph.AdjacentVertices(graph, v)
5080
- neighbor_labels = sorted(labels.get(n, "0") for n in neighbors)
5081
- long_label = labels[v] + "_" + "_".join(neighbor_labels)
5082
- hashed_label = hashlib.md5(long_label.encode()).hexdigest()
5083
- new_labels[v] = hashed_label
5084
- all_label_counts[hashed_label] += 1
5085
- labels = new_labels
5086
-
5087
- return all_label_counts
5088
-
5089
- def weisfeiler_lehman_similarity(graphA, graphB, iterations=3, mantissa=6):
5090
- f1 = weisfeiler_lehman_fingerprint(graphA, iterations)
5091
- f2 = weisfeiler_lehman_fingerprint(graphB, iterations)
5092
-
5093
- common_labels = set(f1.keys()) & set(f2.keys())
5094
- score = sum(min(f1[label], f2[label]) for label in common_labels)
5095
- norm = max(sum(f1.values()), sum(f2.values()), 1)
5096
-
5097
- 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
5098
5071
 
5099
5072
  if not Topology.IsInstance(graphA, "graph"):
5100
5073
  if not silent:
@@ -5130,7 +5103,7 @@ class Graph:
5130
5103
  jaccard_score = weighted_jaccard_similarity(graphA, graphB, vertexIDKey=vertexIDKey, edgeWeightKey=edgeWeightKey, mantissa=mantissa) if weightJaccard else 0
5131
5104
  pagerank_score = pagerank_similarity(graphA, graphB, mantissa=mantissa) if weightPageRank else 0
5132
5105
  structure_score = structure_similarity(graphA, graphB, mantissa=mantissa) if weightStructure else 0
5133
- 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
5134
5107
 
5135
5108
  weighted_sum = (
5136
5109
  accessibility_centrality_score * weightAccessibilityCentrality +
@@ -13919,6 +13892,171 @@ class Graph:
13919
13892
  diffBA = Graph.Difference(graphB, graphA, vertexKeys=vertexKeys, useCentroid=useCentroid, tolerance=tolerance, silent=True)
13920
13893
  return Graph.Union(diffAB, diffBA, vertexKeys=vertexKeys, useCentroid=useCentroid, tolerance=tolerance, silent=True)
13921
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
+
13922
14060
  @staticmethod
13923
14061
  def XOR(graphA, graphB, vertexKeys, useCentroid: bool = False, tolerance: float = 0.001, silent: bool = False):
13924
14062
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.8.45'
1
+ __version__ = '0.8.46'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.45
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=Qwc8SfAzoWGj_Op77QNyIVRx2iL79NJOkhO4Uu_xogE,655808
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=gbj_9V6jOG2ld32r54uMjnVmRZWWapq9d0kXO6shk_E,23
34
- topologicpy-0.8.45.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
35
- topologicpy-0.8.45.dist-info/METADATA,sha256=wbkQITLteNvtJg6ctFuuwYjzLn3S-RC-_UhqZ9BWQzI,10535
36
- topologicpy-0.8.45.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
37
- topologicpy-0.8.45.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
38
- topologicpy-0.8.45.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,,