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 +214 -0
- topologicpy/version.py +1 -1
- {topologicpy-0.8.19.dist-info → topologicpy-0.8.20.dist-info}/METADATA +1 -1
- {topologicpy-0.8.19.dist-info → topologicpy-0.8.20.dist-info}/RECORD +7 -7
- {topologicpy-0.8.19.dist-info → topologicpy-0.8.20.dist-info}/WHEEL +0 -0
- {topologicpy-0.8.19.dist-info → topologicpy-0.8.20.dist-info}/licenses/LICENSE +0 -0
- {topologicpy-0.8.19.dist-info → topologicpy-0.8.20.dist-info}/top_level.txt +0 -0
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.
|
1
|
+
__version__ = '0.8.20'
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: topologicpy
|
3
|
-
Version: 0.8.
|
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=
|
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
|
32
|
-
topologicpy-0.8.
|
33
|
-
topologicpy-0.8.
|
34
|
-
topologicpy-0.8.
|
35
|
-
topologicpy-0.8.
|
36
|
-
topologicpy-0.8.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|