topologicpy 0.8.19__py3-none-any.whl → 0.8.20__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
@@ -4377,6 +4377,220 @@ class Graph:
4377
4377
  v = Topology.SetDictionary(v, d)
4378
4378
  return graph
4379
4379
 
4380
+ @staticmethod
4381
+ def Compare(graphA, graphB,
4382
+ weightAttributes: float = 1.0,
4383
+ weightGeometry: float = 1.0,
4384
+ weightMetrics: float = 1.0,
4385
+ weightStructure: float = 1.0,
4386
+ weightWL: float = 1.0,
4387
+ iterations: int = 3,
4388
+ mantissa: int = 6,
4389
+ silent: bool = False):
4390
+ """
4391
+ Compares two graphs and returns a similarity score based on attributres, geometry, metrics, structure, and
4392
+ the Weisfeiler-Lehman graph kernel. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
4393
+
4394
+ Parameters
4395
+ ----------
4396
+ graphA : topologic Graph
4397
+ The first input graph.
4398
+ graphB : topologic Graph
4399
+ The second input graph.
4400
+ weightAttributes : float , optional
4401
+ The desired weight for attribute similarity (dictionary key overlap at vertices). Default is 1.0.
4402
+ weightGeometry : float , optional
4403
+ The desired weight for geometric similarity (vertex positions). Default is 1.0.
4404
+ weightMetrics : float , optional
4405
+ The desired weight for metric similarity (graph-level and node-level). Default is 1.0.
4406
+ The compared metrics are: betweenness centrality, closeness centrality, clustering coefficient, degree, diameter, and pagerank.
4407
+ weightStructure : float , optional
4408
+ The desired weight for structural similarity (number of vertices and edges). Default is 1.0.
4409
+ weightWL : float , optional
4410
+ The desired weight for Weisfeiler-Lehman kernel similarity (iterative label propagation). Default is 1.0.
4411
+ iterations : int , optional
4412
+ The desired number of Weisfeiler-Lehman iterations. Default is 3.
4413
+ mantissa : int , optional
4414
+ The desired length of the mantissa. The default is 6.
4415
+ silent : bool , optional
4416
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
4417
+
4418
+ Returns
4419
+ -------
4420
+ dict
4421
+ A dictionary of similarity scores between 0 (completely dissimilar) and 1 (identical), based on weighted components.
4422
+ The keys in the dictionary are:
4423
+ "attribute"
4424
+ "geometry"
4425
+ "metrics"
4426
+ "structure"
4427
+ "wl"
4428
+ "overall"
4429
+
4430
+ """
4431
+
4432
+ import hashlib
4433
+ from collections import Counter
4434
+ from topologicpy.Graph import Graph
4435
+ from topologicpy.Vertex import Vertex
4436
+ from topologicpy.Topology import Topology
4437
+ from topologicpy.Dictionary import Dictionary
4438
+
4439
+ def attribute_similarity(graphA, graphB, mantissa=6):
4440
+ v1 = Graph.Vertices(graphA)
4441
+ v2 = Graph.Vertices(graphB)
4442
+ if len(v1) != len(v2) or len(v1) == 0:
4443
+ return 0
4444
+
4445
+ match_score = 0
4446
+ for a, b in zip(v1, v2):
4447
+ dict_a = Topology.Dictionary(a)
4448
+ dict_b = Topology.Dictionary(b)
4449
+
4450
+ keys_a = set(Dictionary.Keys(dict_a)) if dict_a else set()
4451
+ keys_b = set(Dictionary.Keys(dict_b)) if dict_b else set()
4452
+
4453
+ if not keys_a and not keys_b:
4454
+ match_score += 1
4455
+ else:
4456
+ intersection = len(keys_a & keys_b)
4457
+ union = len(keys_a | keys_b)
4458
+ match_score += intersection / union if union > 0 else 0
4459
+
4460
+ return round(match_score / len(v1), mantissa)
4461
+
4462
+ def geometry_similarity(graphA, graphB, mantissa=6):
4463
+ v1 = Graph.Vertices(graphA)
4464
+ v2 = Graph.Vertices(graphB)
4465
+ if len(v1) != len(v2) or len(v1) == 0:
4466
+ return 0
4467
+
4468
+ total_dist = 0
4469
+ for a, b in zip(v1, v2): # assumes same order
4470
+ p1 = Vertex.Coordinates(a, mantissa=mantissa)
4471
+ p2 = Vertex.Coordinates(b, mantissa=mantissa)
4472
+ total_dist += sum((i - j) ** 2 for i, j in zip(p1, p2)) ** 0.5
4473
+ avg_dist = total_dist / len(v1)
4474
+ return round(1 / (1 + avg_dist), mantissa) # Inverse average distance
4475
+
4476
+ def metrics_similarity(graphA, graphB, mantissa=6):
4477
+ # Example using global metrics + mean of node metrics
4478
+ def safe_mean(lst):
4479
+ return sum(lst)/len(lst) if lst else 0
4480
+
4481
+ metrics1 = {
4482
+ "closeness": safe_mean(Graph.ClosenessCentrality(graphA)),
4483
+ "betweenness": safe_mean(Graph.BetweennessCentrality(graphA)),
4484
+ "degree": safe_mean(Graph.DegreeCentrality(graphA)),
4485
+ "pagerank": safe_mean(Graph.PageRank(graphA)),
4486
+ "clustering": Graph.GlobalClusteringCoefficient(graphA),
4487
+ "diameter": Graph.Diameter(graphA)
4488
+ }
4489
+
4490
+ metrics2 = {
4491
+ "closeness": safe_mean(Graph.ClosenessCentrality(graphB)),
4492
+ "betweenness": safe_mean(Graph.BetweennessCentrality(graphB)),
4493
+ "degree": safe_mean(Graph.DegreeCentrality(graphB)),
4494
+ "pagerank": safe_mean(Graph.PageRank(graphB)),
4495
+ "clustering": Graph.GlobalClusteringCoefficient(graphB),
4496
+ "diameter": Graph.Diameter(graphB)
4497
+ }
4498
+
4499
+ # Compute similarity as 1 - normalized absolute difference
4500
+ similarities = []
4501
+ for key in metrics1:
4502
+ v1, v2 = metrics1[key], metrics2[key]
4503
+ if v1 == 0 and v2 == 0:
4504
+ similarities.append(1)
4505
+ else:
4506
+ diff = abs(v1 - v2) / max(abs(v1), abs(v2), 1e-6)
4507
+ similarities.append(1 - diff)
4508
+
4509
+ return round(sum(similarities) / len(similarities), mantissa)
4510
+
4511
+ def structure_similarity(graphA, graphB, mantissa=6):
4512
+ v1 = Graph.Vertices(graphA)
4513
+ v2 = Graph.Vertices(graphB)
4514
+ e1 = Graph.Edges(graphA)
4515
+ e2 = Graph.Edges(graphB)
4516
+
4517
+ vertex_score = 1 - abs(len(v1) - len(v2)) / max(len(v1), len(v2), 1)
4518
+ edge_score = 1 - abs(len(e1) - len(e2)) / max(len(e1), len(e2), 1)
4519
+
4520
+ return round((vertex_score + edge_score) / 2, mantissa)
4521
+
4522
+ def weisfeiler_lehman_fingerprint(graph, iterations=3):
4523
+ vertices = Graph.Vertices(graph)
4524
+ labels = {}
4525
+
4526
+ for v in vertices:
4527
+ d = Topology.Dictionary(v)
4528
+ label = str(Dictionary.ValueAtKey(d, "label")) if d and Dictionary.ValueAtKey(d, "label") else "0"
4529
+ labels[v] = label
4530
+
4531
+ all_label_counts = Counter()
4532
+
4533
+ for _ in range(iterations):
4534
+ new_labels = {}
4535
+ for v in vertices:
4536
+ neighbors = Graph.AdjacentVertices(graph, v)
4537
+ neighbor_labels = sorted(labels.get(n, "0") for n in neighbors)
4538
+ long_label = labels[v] + "_" + "_".join(neighbor_labels)
4539
+ hashed_label = hashlib.md5(long_label.encode()).hexdigest()
4540
+ new_labels[v] = hashed_label
4541
+ all_label_counts[hashed_label] += 1
4542
+ labels = new_labels
4543
+
4544
+ return all_label_counts
4545
+
4546
+ def weisfeiler_lehman_similarity(graphA, graphB, iterations=3, mantissa=6):
4547
+ f1 = weisfeiler_lehman_fingerprint(graphA, iterations)
4548
+ f2 = weisfeiler_lehman_fingerprint(graphB, iterations)
4549
+
4550
+ common_labels = set(f1.keys()) & set(f2.keys())
4551
+ score = sum(min(f1[label], f2[label]) for label in common_labels)
4552
+ norm = max(sum(f1.values()), sum(f2.values()), 1)
4553
+
4554
+ return round(score / norm, mantissa)
4555
+
4556
+ if not Topology.IsInstance(graphA, "graph"):
4557
+ if not silent:
4558
+ print("Graph.Compare - Error: The graphA input parameter is not a valid topologic graph. Returning None.")
4559
+ return None
4560
+ if not Topology.IsInstance(graphB, "graph"):
4561
+ if not silent:
4562
+ print("Graph.Compare - Error: The graphB input parameter is not a valid topologic graph. Returning None.")
4563
+ return
4564
+
4565
+ total_weight = weightAttributes + weightGeometry + weightMetrics + weightStructure + weightWL
4566
+
4567
+ attribute_score = attribute_similarity(graphA, graphB, mantissa=mantissa) if weightAttributes else 0
4568
+ geometry_score = geometry_similarity(graphA, graphB, mantissa=mantissa) if weightGeometry else 0
4569
+ metrics_score = metrics_similarity(graphA, graphB, mantissa=mantissa) if weightMetrics else 0
4570
+ structure_score = structure_similarity(graphA, graphB, mantissa=mantissa) if weightStructure else 0
4571
+ wl_score = weisfeiler_lehman_similarity(graphA, graphB, iterations, mantissa=mantissa) if weightWL else 0
4572
+
4573
+ weighted_sum = (
4574
+ attribute_score * weightAttributes +
4575
+ geometry_score * weightGeometry +
4576
+ metrics_score * weightMetrics +
4577
+ structure_score * weightStructure +
4578
+ wl_score * weightWL
4579
+ )
4580
+
4581
+ if total_weight <= 0:
4582
+ overall_score = 0
4583
+ else:
4584
+ overall_score = weighted_sum / total_weight
4585
+
4586
+ return { "attribute": round(attribute_score, mantissa),
4587
+ "geometry": round(geometry_score, mantissa),
4588
+ "metrics": round(metrics_score, mantissa),
4589
+ "structure": round(structure_score, mantissa),
4590
+ "wl": round(wl_score, mantissa),
4591
+ "overall": round(overall_score, mantissa)
4592
+ }
4593
+
4380
4594
  @staticmethod
4381
4595
  def Complement(graph, tolerance=0.0001, silent=False):
4382
4596
  """
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.8.19'
1
+ __version__ = '0.8.20'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: topologicpy
3
- Version: 0.8.19
3
+ Version: 0.8.20
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
@@ -11,7 +11,7 @@ topologicpy/Dictionary.py,sha256=7h-Gszgnt2OEOvOSADJ4pa-mTNlhQ9cuIiB5WHEW6aY,339
11
11
  topologicpy/Edge.py,sha256=yxkCVDYBflJNEYxnjMmlyvbkpg8TNy7y5bSH3yQ4jzs,71418
12
12
  topologicpy/EnergyModel.py,sha256=UoQ9Jm-hYsN383CbcLKw-y6BKitRHj0uyh84yQ-8ACg,53856
13
13
  topologicpy/Face.py,sha256=SlhB8L7BpDjd4a9YZE4UJ3zoGuF1oq9MSpuesEWro_Q,184847
14
- topologicpy/Graph.py,sha256=FOG3TyXfA1K9c4V3PXKP2vbO2L7HOnGj28pAVnuVRqo,505547
14
+ topologicpy/Graph.py,sha256=fsWgbYp1di1HtDipsXY9d2i42Fn_3dXsVKTac9vl7bU,515137
15
15
  topologicpy/Grid.py,sha256=2s9cSlWldivn1i9EUz4OOokJyANveqmRe_vR93CAndI,18245
16
16
  topologicpy/Helper.py,sha256=4H5KPiv_eiEs489UOOyGLe9RaeoZIfmMh3mk_YCHmXg,29100
17
17
  topologicpy/Honeybee.py,sha256=uDVtDbloydNoaBFcSNukKL_2PLyD6XKkCp1VHz1jtaU,21751
@@ -28,9 +28,9 @@ topologicpy/Vector.py,sha256=GkGt-aJ591IJ2IPffMAudvITLDPi2qZibZc4UAav6m8,42407
28
28
  topologicpy/Vertex.py,sha256=q99IrWwdNlvVfUXz6iP8icmP8NzP2nsiqtgnF9Jnpx8,80913
29
29
  topologicpy/Wire.py,sha256=IVPzBKsckuxC-rHvwxmvtVF7PpApz2lhPHomtQMo_kg,228499
30
30
  topologicpy/__init__.py,sha256=vlPCanUbxe5NifC4pHcnhSzkmmYcs_UrZrTlVMsxcFs,928
31
- topologicpy/version.py,sha256=-CEaM_uLk3M3OyXlLqkLmTj4IViw6vFkTKIRWVX31LI,23
32
- topologicpy-0.8.19.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.8.19.dist-info/METADATA,sha256=pFjIJ2TfH28CMrW4rFY9OZMo-4ROp3hzpRns8o6CF08,10535
34
- topologicpy-0.8.19.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
35
- topologicpy-0.8.19.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.8.19.dist-info/RECORD,,
31
+ topologicpy/version.py,sha256=uWqZIJblLUrle0kYZkrKO4aVSNXiyYenL0G_TSUJllE,23
32
+ topologicpy-0.8.20.dist-info/licenses/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
+ topologicpy-0.8.20.dist-info/METADATA,sha256=0_3vo7ubfFiV0woZaJt8RCFHF9MZZ_Sc7VnqYZX4Mao,10535
34
+ topologicpy-0.8.20.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
35
+ topologicpy-0.8.20.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
+ topologicpy-0.8.20.dist-info/RECORD,,