topologicpy 0.7.72__py3-none-any.whl → 0.7.74__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.
@@ -1006,6 +1006,64 @@ class CellComplex():
1006
1006
  shells = Topology.Shells(cellComplex)
1007
1007
  return shells
1008
1008
 
1009
+ @staticmethod
1010
+ def Torus(origin= None, majorRadius: float = 0.5, minorRadius: float = 0.125, uSides: int = 16, vSides: int = 8, direction: list = [0, 0, 1], placement: str = "center", tolerance: float = 0.0001):
1011
+ """
1012
+ Creates a torus.
1013
+
1014
+ Parameters
1015
+ ----------
1016
+ origin : topologic_core.Vertex , optional
1017
+ The origin location of the torus. The default is None which results in the torus being placed at (0, 0, 0).
1018
+ majorRadius : float , optional
1019
+ The major radius of the torus. The default is 0.5.
1020
+ minorRadius : float , optional
1021
+ The minor radius of the torus. The default is 0.1.
1022
+ uSides : int , optional
1023
+ The number of sides along the longitude of the torus. The default is 16.
1024
+ vSides : int , optional
1025
+ The number of sides along the latitude of the torus. The default is 8.
1026
+ direction : list , optional
1027
+ The vector representing the up direction of the torus. The default is [0, 0, 1].
1028
+ placement : str , optional
1029
+ The description of the placement of the origin of the torus. This can be "bottom", "center", or "lowerleft". It is case insensitive. The default is "center".
1030
+ tolerance : float , optional
1031
+ The desired tolerance. The default is 0.0001.
1032
+
1033
+ Returns
1034
+ -------
1035
+ topologic_core.Cell
1036
+ The created torus.
1037
+
1038
+ """
1039
+
1040
+ from topologicpy.Vertex import Vertex
1041
+ from topologicpy.Wire import Wire
1042
+ from topologicpy.Face import Face
1043
+ from topologicpy.Cell import Cell
1044
+ from topologicpy.Topology import Topology
1045
+
1046
+ if not Topology.IsInstance(origin, "Vertex"):
1047
+ origin = Vertex.ByCoordinates(0, 0, 0)
1048
+ if not Topology.IsInstance(origin, "Vertex"):
1049
+ print("Cell.Torus - Error: The input origin parameter is not a valid topologic vertex. Returning None.")
1050
+ return None
1051
+ c = Wire.Circle(origin=Vertex.Origin(), radius=minorRadius, sides=vSides, fromAngle=0, toAngle=360, close=False, direction=[0, 1, 0], placement="center")
1052
+ c = Face.ByWire(c)
1053
+ c = Topology.Translate(c, abs(majorRadius-minorRadius), 0, 0)
1054
+ torus = Topology.Spin(c, origin=Vertex.Origin(), triangulate=False, direction=[0, 0, 1], angle=360, sides=uSides, tolerance=tolerance)
1055
+ if Topology.Type(torus) == Topology.TypeID("Shell"):
1056
+ faces = Topology.Faces(torus)
1057
+ torus = CellComplex.ByFaces(faces)
1058
+ if placement.lower() == "bottom":
1059
+ torus = Topology.Translate(torus, 0, 0, minorRadius)
1060
+ elif placement.lower() == "lowerleft":
1061
+ torus = Topology.Translate(torus, majorRadius, majorRadius, minorRadius)
1062
+
1063
+ torus = Topology.Orient(torus, origin=Vertex.Origin(), dirA=[0, 0, 1], dirB=direction)
1064
+ torus = Topology.Place(torus, originA=Vertex.Origin(), originB=origin)
1065
+ return torus
1066
+
1009
1067
  @staticmethod
1010
1068
  def Vertices(cellComplex) -> list:
1011
1069
  """
topologicpy/Graph.py CHANGED
@@ -742,6 +742,102 @@ class Graph:
742
742
  _ = graph.AllPaths(vertexA, vertexB, True, timeLimit, paths) # Hook to Core
743
743
  return paths
744
744
 
745
+ @staticmethod
746
+ def AreIsomorphic(graphA, graphB, maxIterations=10, silent=False):
747
+ """
748
+ Tests if the two input graphs are isomorphic according to the Weisfeiler Lehman graph isomorphism test. See https://en.wikipedia.org/wiki/Weisfeiler_Leman_graph_isomorphism_test
749
+
750
+ Parameters
751
+ ----------
752
+ graphA : topologic_core.Graph
753
+ The first input graph.
754
+ graphB : topologic_core.Graph
755
+ The second input graph.
756
+ maxIterations : int , optional
757
+ This number limits the number of iterations to prevent the function from running indefinitely, particularly for very large or complex graphs.
758
+ silent : bool , optional
759
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
760
+
761
+ Returns
762
+ -------
763
+ bool
764
+ True if the two input graphs are isomorphic. False otherwise
765
+
766
+ """
767
+
768
+ from topologicpy.Topology import Topology
769
+
770
+ def weisfeiler_lehman_test(graph1, graph2, max_iterations=10):
771
+ """
772
+ Test if two graphs are isomorphic using the Weisfeiler-Leman (WL) algorithm with early stopping.
773
+
774
+ Parameters:
775
+ graph1 (dict): Adjacency list representation of the first graph.
776
+ graph2 (dict): Adjacency list representation of the second graph.
777
+ max_iterations (int): Maximum WL iterations allowed (default is 10).
778
+
779
+ Returns:
780
+ bool: True if the graphs are WL-isomorphic, False otherwise.
781
+ """
782
+
783
+ def wl_iteration(labels, graph):
784
+ """Perform one WL iteration and return updated labels."""
785
+ new_labels = {}
786
+ for node in graph:
787
+ neighborhood_labels = sorted([labels[neighbor] for neighbor in graph[node]])
788
+ new_labels[node] = (labels[node], tuple(neighborhood_labels))
789
+ unique_labels = {}
790
+ count = 0
791
+ for node in sorted(new_labels):
792
+ if new_labels[node] not in unique_labels:
793
+ unique_labels[new_labels[node]] = count
794
+ count += 1
795
+ new_labels[node] = unique_labels[new_labels[node]]
796
+ return new_labels
797
+
798
+ # Initialize labels
799
+ labels1 = {node: 1 for node in graph1}
800
+ labels2 = {node: 1 for node in graph2}
801
+
802
+ for i in range(max_iterations):
803
+ # Perform WL iteration for both graphs
804
+ new_labels1 = wl_iteration(labels1, graph1)
805
+ new_labels2 = wl_iteration(labels2, graph2)
806
+
807
+ # Check if the label distributions match
808
+ if sorted(new_labels1.values()) != sorted(new_labels2.values()):
809
+ return False
810
+
811
+ # Check for stability (early stopping)
812
+ if new_labels1 == labels1 and new_labels2 == labels2:
813
+ break
814
+
815
+ # Update labels for next iteration
816
+ labels1, labels2 = new_labels1, new_labels2
817
+
818
+ return True
819
+
820
+ if not Topology.IsInstance(graphA, "Graph") and not Topology.IsInstance(graphB, "Graph"):
821
+ if not silent:
822
+ print("Graph.AreIsomorphic - Error: The input graph parameters are not valid graphs. Returning None.")
823
+ return None
824
+ if not Topology.IsInstance(graphA, "Graph"):
825
+ if not silent:
826
+ print("Graph.AreIsomorphic - Error: The input graphA parameter is not a valid graph. Returning None.")
827
+ return None
828
+ if not Topology.IsInstance(graphB, "Graph"):
829
+ if not silent:
830
+ print("Graph.AreIsomorphic - Error: The input graphB parameter is not a valid graph. Returning None.")
831
+ return None
832
+ if maxIterations <= 0:
833
+ if not silent:
834
+ print("Graph.AreIsomorphic - Error: The input maxIterations parameter is not within a valid range. Returning None.")
835
+ return None
836
+
837
+ g1 = Graph.AdjacencyDictionary(graphA)
838
+ g2 = Graph.AdjacencyDictionary(graphB)
839
+ return weisfeiler_lehman_test(g1, g2, max_iterations=maxIterations)
840
+
745
841
  @staticmethod
746
842
  def AverageClusteringCoefficient(graph, mantissa: int = 6, silent: bool = False):
747
843
  """
@@ -1277,6 +1373,7 @@ class Graph:
1277
1373
  print("Graph.BetweenessCentrality - Error: The input graph is not a valid graph. Returning None.")
1278
1374
  return None
1279
1375
  graphVertices = Graph.Vertices(graph)
1376
+
1280
1377
  if not isinstance(vertices, list):
1281
1378
  vertices = graphVertices
1282
1379
  else:
@@ -1298,7 +1395,15 @@ class Graph:
1298
1395
  if len(destinations) < 1:
1299
1396
  print("Graph.BetweenessCentrality - Error: The input list of destinations does not contain valid vertices. Returning None.")
1300
1397
  return None
1301
-
1398
+ graphEdges = Graph.Edges(graph)
1399
+ if len(graphEdges) == 0:
1400
+ print("Graph.BetweenessCentrality - Warning: The input graph is a null graph.")
1401
+ scores = [0 for t in vertices]
1402
+ for i, v in enumerate(vertices):
1403
+ d = Topology.Dictionary(v)
1404
+ d = Dictionary.SetValueAtKey(d, key, scores[i])
1405
+ v = Topology.SetDictionary(v, d)
1406
+ return scores
1302
1407
  paths = []
1303
1408
  try:
1304
1409
  for so in tqdm(sources, desc="Computing Shortest Paths", leave=False):
@@ -1906,8 +2011,6 @@ class Graph:
1906
2011
  for i, vertices, in enumerate(vertices_ds):
1907
2012
  edges = edges_ds[i]
1908
2013
  g = Graph.ByVerticesEdges(vertices, edges)
1909
- temp_v = Graph.Vertices(g)
1910
- temp_e = Graph.Edges(g)
1911
2014
  if Topology.IsInstance(g, "Graph"):
1912
2015
  if len(graphFeaturesKeys) == 0:
1913
2016
  values = [graph_ids[i], graph_labels[i], graph_features[i]]
@@ -5526,19 +5629,35 @@ class Graph:
5526
5629
  return False
5527
5630
 
5528
5631
  @staticmethod
5529
- def Flatten(graph, layout="spring", k=0.8, seed=None, iterations=50, rootVertex=None, radius=0.5, tolerance=0.0001):
5530
- """
5531
- Flattens the input graph.
5632
+ def Reshape(graph,
5633
+ shape="spring_2d",
5634
+ k=0.8, seed=None,
5635
+ iterations=50,
5636
+ rootVertex=None,
5637
+ size=1,
5638
+ sides=16,
5639
+ key="",
5640
+ tolerance=0.0001,
5641
+ silent=False):
5642
+ """
5643
+ Reshapes the input graph according to the desired input shape parameter.
5532
5644
 
5533
5645
  Parameters
5534
5646
  ----------
5535
5647
  graph : topologic_core.Graph
5536
5648
  The input graph.
5537
- layout : str , optional
5538
- The desired mode for flattening. If set to 'spring', the algorithm uses a simplified version of the Fruchterman-Reingold force-directed algorithm to flatten and distribute the vertices.
5539
- If set to 'radial', the nodes will be distributed along concentric circles.
5540
- If set to 'tree', the nodes will be distributed using the Reingold-Tillford layout.
5541
- If set to 'circle', the nodes will be distributed on the cirumference of a circle. The default is 'spring'.
5649
+ shape : str , optional
5650
+ The desired shape of the graph.
5651
+ If set to 'spring_2d' or 'spring_3d', the algorithm uses a simplified version of the Fruchterman-Reingold force-directed algorithm to distribute the vertices.
5652
+ If set to 'radial_2d', the nodes will be distributed along concentric circles in the XY plane.
5653
+ If set to 'tree_2d' or 'tree_3d', the nodes will be distributed using the Reingold-Tillford layout.
5654
+ If set to 'circle_2d', the nodes will be distributed on the cirumference of a segemented circles in the XY plane, based on the size and sides input parameter (radius=size/2).
5655
+ If set to 'line_2d', the nodes will be distributed on a line in the XY plane based on the size input parameter (length=size).
5656
+ If set to 'spehere_3d', the nodes will be distributed on the surface of a sphere based on the size input parameter raidus=size/2).
5657
+ If set to 'grid_2d', the nodes will be distributed on a grid in the XY plane with size based on the size input parameter (length=width=size).
5658
+ If set to 'grid_3d', the nodes will be distributed on a 3D cubic grid/matrix based on the size input parameter(width=length=height=size).
5659
+ If set to 'cluster_2d', or 'cluster_3d, the nodes will be clustered according to the 'key' input parameter. The overall radius of the cluster is determined by the size input parameter (radius = size/2)
5660
+ The default is 'spring_2d'.
5542
5661
  k : float, optional
5543
5662
  The desired spring constant to use for the attractive and repulsive forces. The default is 0.8.
5544
5663
  seed : int , optional
@@ -5547,20 +5666,38 @@ class Graph:
5547
5666
  The desired maximum number of iterations to solve the forces in the 'spring' mode. The default is 50.
5548
5667
  rootVertex : topologic_core.Vertex , optional
5549
5668
  The desired vertex to use as the root of the tree and radial layouts.
5550
- radius : float, optional
5551
- The desired radius for the circle layout option. The default is 0.5.
5669
+ sides : int , optional
5670
+ The desired number of sides of the circle layout option. The default is 16
5671
+ length : float, optional
5672
+ The desired horizontal length for the line layout option. The default is 1.0.
5673
+ key : string, optional
5674
+ The key under which to find the clustering value for the 'cluster_2d' and 'cluster_3d' options. The default is "".
5552
5675
  tolerance : float , optional
5553
5676
  The desired tolerance. The default is 0.0001.
5677
+ silent : bool , optional
5678
+ If set to True, no error and warning messages are printed. Otherwise, they are. The default is False.
5554
5679
 
5555
5680
  Returns
5556
5681
  -------
5557
5682
  topologic_core.Graph
5558
- The flattened graph.
5683
+ The reshaped graph.
5559
5684
 
5560
5685
  """
5561
5686
  from topologicpy.Vertex import Vertex
5687
+ from topologicpy.Edge import Edge
5688
+ from topologicpy.Wire import Wire
5689
+ from topologicpy.Face import Face
5690
+ from topologicpy.Graph import Graph
5691
+ from topologicpy.Grid import Grid
5692
+ from topologicpy.Helper import Helper
5693
+ from topologicpy.Vector import Vector
5562
5694
  from topologicpy.Topology import Topology
5695
+ from topologicpy.Dictionary import Dictionary
5563
5696
  import numpy as np
5697
+ import math
5698
+ from collections import defaultdict
5699
+ import random
5700
+
5564
5701
 
5565
5702
  def buchheim(tree):
5566
5703
  dt = firstwalk(_DrawTree(tree))
@@ -5674,7 +5811,7 @@ class Graph:
5674
5811
  Returns:
5675
5812
  A numpy array representing the adjacency matrix.
5676
5813
  """
5677
- from topologicpy.Helper import Helper
5814
+
5678
5815
  # Get the number of nodes from the edge list.
5679
5816
  flat_list = Helper.Flatten(edge_list)
5680
5817
  flat_list = [x for x in flat_list if not x == None]
@@ -5714,15 +5851,226 @@ class Graph:
5714
5851
  new_roots.extend(children)
5715
5852
  old_roots = new_roots
5716
5853
  return root, num_nodes
5854
+
5855
+ def generate_cubic_matrix(size, min_points):
5856
+ # Calculate the minimum points per axis to reach or exceed min_points in total
5857
+ points_per_axis = int(np.ceil(min_points ** (1/3)))
5858
+
5859
+ # Calculate the spacing based on the size and points per axis
5860
+ spacing = size / (points_per_axis - 1) if points_per_axis > 1 else 0
5861
+
5862
+ # Generate linearly spaced points from -size/2 to size/2 along each axis
5863
+ x = np.linspace(-size / 2, size / 2, points_per_axis)
5864
+ y = np.linspace(-size / 2, size / 2, points_per_axis)
5865
+ z = np.linspace(-size / 2, size / 2, points_per_axis)
5866
+
5867
+ # Create a meshgrid and stack them to get XYZ coordinates for each point
5868
+ X, Y, Z = np.meshgrid(x, y, z)
5869
+ points = np.vstack([X.ravel(), Y.ravel(), Z.ravel()]).T
5870
+ return points
5871
+
5872
+ def vertex_max_degree(graph, vertices):
5873
+ degrees = [Graph.VertexDegree(graph, vertex) for vertex in vertices]
5874
+ i = degrees.index(max(degrees))
5875
+ return vertices[i], i
5876
+
5877
+ def circle_layout_2d(graph, radius=0.5, sides=16):
5878
+ vertices = Graph.Vertices(graph)
5879
+ edges = Graph.Edges(graph)
5880
+ edge_dict = {}
5717
5881
 
5882
+ for i, edge in enumerate(edges):
5883
+ sv = Edge.StartVertex(edge)
5884
+ ev = Edge.EndVertex(edge)
5885
+ si = Vertex.Index(sv, vertices)
5886
+ ei = Vertex.Index(ev, vertices)
5887
+ edge_dict[str(si)+"_"+str(ei)] = i
5888
+ edge_dict[str(ei)+"_"+str(si)] = i
5889
+ n = len(vertices)
5890
+ c = Wire.Circle(radius=radius, sides=sides)
5891
+ c_vertices = []
5892
+ for i in range(n):
5893
+ u = i*(1/n)
5894
+ c_vertices.append(Wire.VertexByParameter(c, u))
5718
5895
 
5719
- def circle_layout(graph, radius=0.5):
5720
- from topologicpy.Vertex import Vertex
5721
- from topologicpy.Vector import Vector
5722
- from topologicpy.Wire import Wire
5723
- from topologicpy.Graph import Graph
5724
- from topologicpy.Edge import Edge
5896
+ for i, c_v in enumerate(c_vertices):
5897
+ d = Topology.Dictionary(vertices[i])
5898
+ c_v = Topology.SetDictionary(c_v, d)
5899
+ adj_dict = Graph.AdjacencyDictionary(graph)
5900
+ keys = adj_dict.keys()
5901
+
5902
+ c_edges = []
5903
+ used = [[0] * n for _ in range(n)]
5904
+ for key in keys:
5905
+ x = int(key)
5906
+ adj_vertices = [int(v) for v in adj_dict[key]]
5907
+ for y in adj_vertices:
5908
+ if used[x][y] == 0:
5909
+ v1 = Vector.ByCoordinates(Vertex.X(c_vertices[x]), Vertex.Y(c_vertices[x]), Vertex.Z(c_vertices[x]))
5910
+ v2 = Vector.ByCoordinates(Vertex.X(c_vertices[y]), Vertex.Y(c_vertices[y]), Vertex.Z(c_vertices[y]))
5911
+ ang1 = Vector.CompassAngle(v1, [0,1,0])
5912
+ ang2 = Vector.CompassAngle(v2, [0,1,0])
5913
+ if ang2-ang1 < 180:
5914
+ e = Edge.ByVertices(c_vertices[x], c_vertices[y])
5915
+ else:
5916
+ e = Edge.ByVertices(c_vertices[y], c_vertices[x])
5917
+ orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
5918
+ if orig_edge_index:
5919
+ d = Topology.Dictionary(edges[orig_edge_index])
5920
+ e = Topology.SetDictionary(e, d)
5921
+ c_edges.append(e)
5922
+ used[x][y] = 1
5923
+ used[y][x] = 1
5924
+ new_g = Graph.ByVerticesEdges(c_vertices, c_edges)
5925
+ return new_g
5926
+
5927
+ def cluster_layout_2d(graph, key, radius=0.5):
5928
+
5929
+ d = Graph.MeshData(graph)
5930
+ edges = d['edges']
5931
+ v_dicts = d['vertexDictionaries']
5932
+ e_dicts = d['edgeDictionaries']
5933
+ vertices = Graph.Vertices(graph)
5934
+ # Step 1: Group objects by key value while remembering their original indices
5935
+ grouped_objects = defaultdict(list)
5936
+ object_indices = [] # Stores original indices of objects in the order they were grouped
5937
+
5938
+ for idx, obj in enumerate(vertices):
5939
+ d = Topology.Dictionary(obj)
5940
+ value = Dictionary.ValueAtKey(d, key)
5941
+ grouped_objects[value].append((obj, idx))
5942
+ object_indices.append((value, idx))
5943
+
5944
+ # Step 2: Compute cluster centers on the circumference of a circle
5945
+ cluster_centers = {}
5946
+ num_clusters = len(grouped_objects)
5947
+
5948
+ # Function to generate cluster center on the circle's circumference
5949
+ def generate_cluster_center(index, total_clusters, circle_radius):
5950
+ # Distribute cluster centers evenly along the circumference of a circle
5951
+ angle = (2 * math.pi * index) / total_clusters # Equal angle separation
5952
+ x = circle_radius * math.cos(angle)
5953
+ y = circle_radius * math.sin(angle)
5954
+ return (x, y)
5955
+
5956
+ # Step 3: Compute vertices for each cluster
5957
+ object_positions = [None] * len(vertices) # Placeholder list for ordered vertices
5958
+ cluster_index = 0
5959
+
5960
+ for value, objs in grouped_objects.items():
5961
+ intra_cluster_radius = radius*(len(objs)/len(vertices) + 0.1)
5962
+ # Determine the center of the current cluster
5963
+ if value not in cluster_centers:
5964
+ cluster_center = generate_cluster_center(cluster_index, num_clusters, radius)
5965
+ cluster_centers[value] = cluster_center
5966
+ cluster_index += 1
5967
+ else:
5968
+ cluster_center = cluster_centers[value]
5969
+
5970
+ # Step 4: Place objects randomly around the cluster center
5971
+ for obj, original_index in objs:
5972
+ # Randomly place the object within the intra-cluster circle
5973
+ r = intra_cluster_radius * math.sqrt(random.random()) # Random distance with sqrt for uniform distribution
5974
+ angle = random.uniform(0, 2 * math.pi) # Random angle
5975
+
5976
+ # Polar coordinates to Cartesian for local positioning
5977
+ x = cluster_center[0] + r * math.cos(angle)
5978
+ y = cluster_center[1] + r * math.sin(angle)
5979
+
5980
+ # Save the coordinates in the correct order
5981
+ object_positions[original_index] = [x, y]
5982
+
5983
+ positions = [[p[0], p[1], 0] for p in object_positions]
5984
+ new_g = Graph.ByMeshData(positions, edges, v_dicts, e_dicts, tolerance=0.001)
5985
+ return new_g
5986
+
5987
+ def cluster_layout_3d(graph, key, radius=0.5):
5988
+ d = Graph.MeshData(graph)
5989
+ edges = d['edges']
5990
+ v_dicts = d['vertexDictionaries']
5991
+ e_dicts = d['edgeDictionaries']
5992
+ vertices = Graph.Vertices(graph)
5993
+
5994
+ # Step 1: Group objects by key value while remembering their original indices
5995
+ grouped_objects = defaultdict(list)
5996
+ object_indices = [] # Stores original indices of objects in the order they were grouped
5997
+
5998
+ for idx, obj in enumerate(vertices):
5999
+ d = Topology.Dictionary(obj)
6000
+ value = Dictionary.ValueAtKey(d, key)
6001
+ grouped_objects[value].append((obj, idx))
6002
+ object_indices.append((value, idx))
6003
+
6004
+ # Step 2: Compute cluster centers on the surface of a sphere
6005
+ cluster_centers = {}
6006
+ num_clusters = len(grouped_objects)
6007
+
6008
+ # Function to generate cluster center on the surface of a sphere
6009
+ def generate_cluster_center(index, total_clusters, sphere_radius):
6010
+ # Use a spiral algorithm to distribute cluster centers evenly on a sphere's surface
6011
+ phi = math.acos(1 - 2 * (index + 0.5) / total_clusters) # Inclination angle
6012
+ theta = math.pi * (1 + 5**0.5) * index # Azimuthal angle (Golden angle)
6013
+
6014
+ x = sphere_radius * math.sin(phi) * math.cos(theta)
6015
+ y = sphere_radius * math.sin(phi) * math.sin(theta)
6016
+ z = sphere_radius * math.cos(phi)
6017
+ return (x, y, z)
6018
+
6019
+ # Step 3: Compute vertices for each cluster
6020
+ object_positions = [None] * len(vertices) # Placeholder list for ordered vertices
6021
+ cluster_index = 0
6022
+
6023
+ for value, objs in grouped_objects.items():
6024
+ # Determine the center of the current cluster
6025
+ if value not in cluster_centers:
6026
+ cluster_center = generate_cluster_center(cluster_index, num_clusters, radius)
6027
+ cluster_centers[value] = cluster_center
6028
+ cluster_index += 1
6029
+ else:
6030
+ cluster_center = cluster_centers[value]
6031
+
6032
+ intra_cluster_radius = radius*(len(objs)/len(vertices) + 0.1)
6033
+
6034
+ # Step 4: Place objects randomly within the cluster's spherical volume
6035
+ for obj, original_index in objs:
6036
+ # Randomly place the object within the intra-cluster sphere
6037
+ u = random.random()
6038
+ v = random.random()
6039
+ r = intra_cluster_radius * (u ** (1/3)) # Random distance with cube root for uniform distribution
6040
+
6041
+ theta = 2 * math.pi * v # Random azimuthal angle
6042
+ phi = math.acos(2 * u - 1) # Random polar angle
6043
+
6044
+ # Spherical to Cartesian for local positioning
6045
+ x = cluster_center[0] + r * math.sin(phi) * math.cos(theta)
6046
+ y = cluster_center[1] + r * math.sin(phi) * math.sin(theta)
6047
+ z = cluster_center[2] + r * math.cos(phi)
6048
+
6049
+ # Save the coordinates in the correct order
6050
+ object_positions[original_index] = [x, y, z]
6051
+
6052
+ positions = [[p[0], p[1], p[2]] for p in object_positions]
6053
+ new_g = Graph.ByMeshData(positions, edges, v_dicts, e_dicts, tolerance=0.001)
6054
+ return new_g
6055
+
6056
+ def sphere_layout_3d(graph, radius=0.5):
6057
+ def points_on_sphere(n, r):
6058
+ points = []
6059
+ phi = math.pi * (3. - math.sqrt(5.)) # Golden angle in radians
5725
6060
 
6061
+ for i in range(n):
6062
+ y = 1 - (i / float(n - 1)) * 2 # y goes from 1 to -1
6063
+ radius = math.sqrt(1 - y * y) # radius at y
6064
+
6065
+ theta = phi * i # Golden angle increment
6066
+
6067
+ x = math.cos(theta) * radius * r
6068
+ z = math.sin(theta) * radius * r
6069
+ y *= r
6070
+
6071
+ points.append([x, y, z])
6072
+ return points
6073
+
5726
6074
  vertices = Graph.Vertices(graph)
5727
6075
  edges = Graph.Edges(graph)
5728
6076
  edge_dict = {}
@@ -5734,16 +6082,15 @@ class Graph:
5734
6082
  ei = Vertex.Index(ev, vertices)
5735
6083
  edge_dict[str(si)+"_"+str(ei)] = i
5736
6084
  edge_dict[str(ei)+"_"+str(si)] = i
5737
-
5738
6085
  n = len(vertices)
5739
- c = Wire.Circle(radius=radius, sides=n)
5740
- c_vertices = Topology.Vertices(c)
5741
-
6086
+ c_points = points_on_sphere(n, r=radius)
6087
+ c_vertices = [Vertex.ByCoordinates(coord) for coord in c_points]
5742
6088
  for i, c_v in enumerate(c_vertices):
5743
6089
  d = Topology.Dictionary(vertices[i])
5744
6090
  c_v = Topology.SetDictionary(c_v, d)
5745
6091
  adj_dict = Graph.AdjacencyDictionary(graph)
5746
6092
  keys = adj_dict.keys()
6093
+
5747
6094
  c_edges = []
5748
6095
  used = [[0] * n for _ in range(n)]
5749
6096
  for key in keys:
@@ -5759,6 +6106,94 @@ class Graph:
5759
6106
  e = Edge.ByVertices(c_vertices[x], c_vertices[y])
5760
6107
  else:
5761
6108
  e = Edge.ByVertices(c_vertices[y], c_vertices[x])
6109
+ orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6110
+ if orig_edge_index:
6111
+ d = Topology.Dictionary(edges[orig_edge_index])
6112
+ e = Topology.SetDictionary(e, d)
6113
+ c_edges.append(e)
6114
+ used[x][y] = 1
6115
+ used[y][x] = 1
6116
+ new_g = Graph.ByVerticesEdges(c_vertices, c_edges)
6117
+ return new_g
6118
+
6119
+ def grid_layout_2d(graph, size=1):
6120
+ vertices = Graph.Vertices(graph)
6121
+ n = len(vertices)
6122
+ u = int(math.sqrt(n))
6123
+ if u*u < n:
6124
+ u += 1
6125
+ u_range = [t/(u-1) for t in range(u)]
6126
+ edges = Graph.Edges(graph)
6127
+ edge_dict = {}
6128
+
6129
+ for i, edge in enumerate(edges):
6130
+ sv = Edge.StartVertex(edge)
6131
+ ev = Edge.EndVertex(edge)
6132
+ si = Vertex.Index(sv, vertices)
6133
+ ei = Vertex.Index(ev, vertices)
6134
+ edge_dict[str(si)+"_"+str(ei)] = i
6135
+ edge_dict[str(ei)+"_"+str(si)] = i
6136
+ f = Face.Rectangle(width=size, length=size)
6137
+ c = Grid.VerticesByParameters(face=f, uRange=u_range, vRange=u_range)
6138
+ c_vertices = Topology.Vertices(c)[:len(vertices)]
6139
+
6140
+ for i, c_v in enumerate(c_vertices):
6141
+ d = Topology.Dictionary(vertices[i])
6142
+ c_v = Topology.SetDictionary(c_v, d)
6143
+ adj_dict = Graph.AdjacencyDictionary(graph)
6144
+ keys = adj_dict.keys()
6145
+
6146
+ c_edges = []
6147
+ used = [[0] * n for _ in range(n)]
6148
+ for key in keys:
6149
+ x = int(key)
6150
+ adj_vertices = [int(v) for v in adj_dict[key]]
6151
+ for y in adj_vertices:
6152
+ if used[x][y] == 0:
6153
+ e = Edge.ByVertices(c_vertices[x], c_vertices[y])
6154
+ orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6155
+ if orig_edge_index:
6156
+ d = Topology.Dictionary(edges[orig_edge_index])
6157
+ e = Topology.SetDictionary(e, d)
6158
+ c_edges.append(e)
6159
+ used[x][y] = 1
6160
+ used[y][x] = 1
6161
+ new_g = Graph.ByVerticesEdges(c_vertices, c_edges)
6162
+ return new_g
6163
+
6164
+ def line_layout_2d(graph, length=1):
6165
+ vertices = Graph.Vertices(graph)
6166
+ edges = Graph.Edges(graph)
6167
+ edge_dict = {}
6168
+
6169
+ for i, edge in enumerate(edges):
6170
+ sv = Edge.StartVertex(edge)
6171
+ ev = Edge.EndVertex(edge)
6172
+ si = Vertex.Index(sv, vertices)
6173
+ ei = Vertex.Index(ev, vertices)
6174
+ edge_dict[str(si)+"_"+str(ei)] = i
6175
+ edge_dict[str(ei)+"_"+str(si)] = i
6176
+
6177
+ n = len(vertices)
6178
+ c = Wire.Line(length=length, sides=n-1)
6179
+ c_vertices = Topology.Vertices(c)
6180
+
6181
+ for i, c_v in enumerate(c_vertices):
6182
+ d = Topology.Dictionary(vertices[i])
6183
+ c_v = Topology.SetDictionary(c_v, d)
6184
+ adj_dict = Graph.AdjacencyDictionary(graph)
6185
+ keys = adj_dict.keys()
6186
+ c_edges = []
6187
+ used = [[0] * n for _ in range(n)]
6188
+ for key in keys:
6189
+ x = int(key)
6190
+ adj_vertices = [int(v) for v in adj_dict[key]]
6191
+ for y in adj_vertices:
6192
+ if used[x][y] == 0:
6193
+ if Vertex.X(c_vertices[x]) < Vertex.X(c_vertices[y]):
6194
+ e = Edge.ByVertices(c_vertices[x], c_vertices[y])
6195
+ else:
6196
+ e = Edge.ByVertices(c_vertices[y], c_vertices[x])
5762
6197
 
5763
6198
  orig_edge_index = edge_dict[str(x)+"_"+str(y)]
5764
6199
  d = Topology.Dictionary(edges[orig_edge_index])
@@ -5769,10 +6204,51 @@ class Graph:
5769
6204
  new_g = Graph.ByVerticesEdges(c_vertices, c_edges)
5770
6205
  return new_g
5771
6206
 
5772
- def spring_layout(edge_list, iterations=500, k=None, seed=None):
5773
- # Compute the layout of a graph using the Fruchterman-Reingold algorithm
5774
- # with a force-directed layout approach.
6207
+ def grid_layout_3d(graph, size=1):
6208
+ vertices = Graph.Vertices(graph)
6209
+ n = len(vertices)
6210
+ edges = Graph.Edges(graph)
6211
+ edge_dict = {}
5775
6212
 
6213
+ for i, edge in enumerate(edges):
6214
+ sv = Edge.StartVertex(edge)
6215
+ ev = Edge.EndVertex(edge)
6216
+ si = Vertex.Index(sv, vertices)
6217
+ ei = Vertex.Index(ev, vertices)
6218
+ edge_dict[str(si)+"_"+str(ei)] = i
6219
+ edge_dict[str(ei)+"_"+str(si)] = i
6220
+ c_coords = generate_cubic_matrix(size, n)
6221
+ c_vertices = [Vertex.ByCoordinates(list(coord)) for coord in c_coords[:n]]
6222
+
6223
+ for i, c_v in enumerate(c_vertices):
6224
+ d = Topology.Dictionary(vertices[i])
6225
+ c_v = Topology.SetDictionary(c_v, d)
6226
+ adj_dict = Graph.AdjacencyDictionary(graph)
6227
+ keys = adj_dict.keys()
6228
+
6229
+ c_edges = []
6230
+ used = [[0] * n for _ in range(n)]
6231
+ for key in keys:
6232
+ x = int(key)
6233
+ adj_vertices = [int(v) for v in adj_dict[key]]
6234
+ for y in adj_vertices:
6235
+ if used[x][y] == 0:
6236
+ e = Edge.ByVertices(c_vertices[x], c_vertices[y])
6237
+ orig_edge_index = edge_dict.get(str(x)+"_"+str(y), edge_dict.get(str(y)+"_"+str(x), None))
6238
+ if orig_edge_index:
6239
+ d = Topology.Dictionary(edges[orig_edge_index])
6240
+ e = Topology.SetDictionary(e, d)
6241
+ c_edges.append(e)
6242
+ used[x][y] = 1
6243
+ used[y][x] = 1
6244
+ new_g = Graph.ByVerticesEdges(c_vertices, c_edges)
6245
+ return new_g
6246
+
6247
+ def spring_layout_2d(edge_list, iterations=500, k=None, seed=None):
6248
+ # Compute the layout of a graph using the Fruchterman-Reingold algorithm
6249
+ # with a force-directed
6250
+
6251
+ iterations = max(1, iterations)
5776
6252
  adj_matrix = edge_list_to_adjacency_matrix(edge_list)
5777
6253
  # Set the random seed
5778
6254
  if seed is not None:
@@ -5820,7 +6296,60 @@ class Graph:
5820
6296
 
5821
6297
  return pos
5822
6298
 
5823
- def tree_layout(edge_list, root_index=0):
6299
+ def spring_layout_3d(edge_list, iterations=500, k=None, seed=None):
6300
+ # Compute the layout of a graph using the Fruchterman-Reingold algorithm
6301
+ # with a force-directed layout approach.
6302
+
6303
+ iterations = max(1,iterations)
6304
+
6305
+ adj_matrix = edge_list_to_adjacency_matrix(edge_list)
6306
+ # Set the random seed
6307
+ if seed is not None:
6308
+ np.random.seed(seed)
6309
+
6310
+ # Set the optimal distance between nodes
6311
+ if k is None or k <= 0:
6312
+ k = np.cbrt(1.0 / adj_matrix.shape[0]) # Adjusted for 3D
6313
+
6314
+ # Initialize the positions of the nodes randomly in 3D
6315
+ pos = np.random.rand(adj_matrix.shape[0], 3)
6316
+
6317
+ # Compute the initial temperature
6318
+ t = 0.1 * np.max(pos)
6319
+
6320
+ # Compute the cooling factor
6321
+ cooling_factor = t / iterations
6322
+
6323
+ # Iterate over the specified number of iterations
6324
+ for i in range(iterations):
6325
+ # Compute the distance between each pair of nodes
6326
+ delta = pos[:, np.newaxis, :] - pos[np.newaxis, :, :]
6327
+ distance = np.linalg.norm(delta, axis=-1)
6328
+
6329
+ # Avoid division by zero
6330
+ distance = np.where(distance == 0, 0.1, distance)
6331
+
6332
+ # Compute the repulsive force between each pair of nodes
6333
+ repulsive_force = k ** 2 / distance ** 2
6334
+
6335
+ # Compute the attractive force between each pair of adjacent nodes
6336
+ attractive_force = adj_matrix * distance / k
6337
+
6338
+ # Compute the total force acting on each node
6339
+ force = np.sum((repulsive_force - attractive_force)[:, :, np.newaxis] * delta, axis=1)
6340
+
6341
+ # Compute the displacement of each node
6342
+ displacement = t * force / np.linalg.norm(force, axis=1)[:, np.newaxis]
6343
+
6344
+ # Update the positions of the nodes
6345
+ pos += displacement
6346
+
6347
+ # Cool the temperature
6348
+ t -= cooling_factor
6349
+
6350
+ return pos
6351
+
6352
+ def tree_layout_2d(edge_list, root_index=0):
5824
6353
 
5825
6354
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
5826
6355
  dt = buchheim(root)
@@ -5846,8 +6375,46 @@ class Graph:
5846
6375
  pos[:, 1] = np.max(pos[:, 1]) - pos[:, 1]
5847
6376
 
5848
6377
  return pos
6378
+
6379
+ def tree_layout_3d(edge_list, root_index=0, base_radius=1.0, radius_factor=1.5):
6380
+ root, num_nodes = tree_from_edge_list(edge_list, root_index)
6381
+ dt = buchheim(root)
6382
+ pos = np.zeros((num_nodes, 3)) # Initialize 3D positions
6383
+
6384
+ pos[int(dt.tree.node), 0] = dt.x
6385
+ pos[int(dt.tree.node), 1] = dt.y
6386
+ pos[int(dt.tree.node), 2] = 0 # Root at z = 0
6387
+
6388
+ old_roots = [dt]
6389
+ new_roots = []
6390
+ depth = 1 # Start at depth level 1 for children
6391
+
6392
+ while len(old_roots) > 0:
6393
+ new_roots = []
6394
+ for temp_root in old_roots:
6395
+ children = temp_root.children
6396
+ num_children = len(children)
6397
+ if num_children > 0:
6398
+ # Increase the radius dynamically based on the number of children
6399
+ dynamic_radius = base_radius + (num_children - 1) * radius_factor * depth
6400
+
6401
+ angle_step = 2 * np.pi / num_children # Angle between each child
6402
+ for i, child in enumerate(children):
6403
+ angle = i * angle_step
6404
+ pos[int(child.tree.node), 0] = pos[int(temp_root.tree.node), 0] + dynamic_radius * np.cos(angle) # X position
6405
+ pos[int(child.tree.node), 1] = pos[int(temp_root.tree.node), 1] + dynamic_radius * np.sin(angle) # Y position
6406
+ pos[int(child.tree.node), 2] = -dynamic_radius*depth # Z-coordinate based on depth
6407
+
6408
+ new_roots.extend(children)
6409
+
6410
+ old_roots = new_roots
6411
+ depth += 1 # Increment depth for the next level
6412
+
6413
+ pos[:, 1] = np.max(pos[:, 1]) - pos[:, 1] # Flip y-coordinates if necessary
6414
+
6415
+ return pos
5849
6416
 
5850
- def radial_layout(edge_list, root_index=0):
6417
+ def radial_layout_2d(edge_list, root_index=0):
5851
6418
  root, num_nodes = tree_from_edge_list(edge_list, root_index)
5852
6419
  dt = buchheim(root)
5853
6420
  pos = np.zeros((num_nodes, 2))
@@ -5888,53 +6455,119 @@ class Graph:
5888
6455
 
5889
6456
  return new_pos
5890
6457
 
5891
- def graph_layout(edge_list, layout='tree', root_index=0, k=None, seed=None, iterations=500):
6458
+ def spherical_layout_3d(edge_list, root_index=0):
6459
+ root, num_nodes = tree_from_edge_list(edge_list, root_index)
6460
+ dt = buchheim(root)
5892
6461
 
5893
- if layout == 'tree':
5894
- return tree_layout(edge_list, root_index=root_index)
5895
- elif layout == 'spring':
5896
- return spring_layout(edge_list, k=k, seed=seed, iterations=iterations)
5897
- elif layout == 'radial':
5898
- return radial_layout(edge_list, root_index=root_index)
5899
- else:
5900
- raise NotImplementedError(f"{layout} is not implemented yet. Please choose from ['radial', 'spring', 'tree']")
6462
+ # Initialize positions with 3 columns for x, y, z coordinates
6463
+ pos = np.zeros((num_nodes, 3))
5901
6464
 
5902
- def vertex_max_degree(graph, vertices):
5903
- degrees = [Graph.VertexDegree(graph, vertex) for vertex in vertices]
5904
- i = degrees.index(max(degrees))
5905
- return vertices[i], i
6465
+ # Set initial coordinates and depth for the root node
6466
+ pos[int(dt.tree.node), 0] = dt.x
6467
+ pos[int(dt.tree.node), 1] = dt.y
6468
+ depth = np.zeros(num_nodes) # To track the depth of each node
6469
+
6470
+ old_roots = [(dt, 0)] # Store nodes with their depth levels
6471
+ new_roots = []
6472
+
6473
+ while len(old_roots) > 0:
6474
+ new_roots = []
6475
+ for temp_root, current_depth in old_roots:
6476
+ children = temp_root.children
6477
+ for child in children:
6478
+ node_index = int(child.tree.node)
6479
+ pos[node_index, 0] = child.x
6480
+ pos[node_index, 1] = child.y
6481
+ depth[node_index] = current_depth + 1 # Increase depth for children
6482
+ new_roots.extend([(child, current_depth + 1) for child in children])
6483
+
6484
+ old_roots = new_roots
6485
+
6486
+ # Normalize x and y coordinates to a [0, 1] range
6487
+ pos[:, 0] = pos[:, 0] - np.min(pos[:, 0])
6488
+ pos[:, 1] = pos[:, 1] - np.min(pos[:, 1])
6489
+
6490
+ pos[:, 0] = pos[:, 0] / np.max(pos[:, 0])
6491
+ pos[:, 0] = pos[:, 0] - pos[:, 0][root_index]
6492
+
6493
+ range_ = np.max(pos[:, 0]) - np.min(pos[:, 0])
6494
+ pos[:, 0] = pos[:, 0] / range_
6495
+
6496
+ pos[:, 0] = pos[:, 0] * np.pi * 1.98 # Longitude (azimuthal angle)
6497
+ pos[:, 1] = pos[:, 1] / np.max(pos[:, 1]) # Latitude (polar angle)
6498
+
6499
+ # Convert the 2D coordinates to 3D spherical coordinates
6500
+ new_pos = np.zeros((num_nodes, 3)) # 3D position array
6501
+ base_radius = 1 # Base radius for the first sphere
6502
+ radius_increment = 1.3 # Increase in radius per depth level
6503
+
6504
+ # pos[:, 0] is the azimuth angle (longitude) in radians
6505
+ # pos[:, 1] is the polar angle (latitude) in radians (adjusted to go from 0 to pi)
6506
+ polar_angle = pos[:, 1] * np.pi # Scaling to go from 0 to pi
6507
+ azimuth_angle = pos[:, 0]
6508
+
6509
+ # Calculate the 3D Cartesian coordinates based on depth
6510
+ for i in range(num_nodes):
6511
+ r = base_radius + depth[i] * radius_increment # Radius grows with depth
6512
+ new_pos[i, 0] = r * np.sin(polar_angle[i]) * np.cos(azimuth_angle[i]) # X = r * sin(θ) * cos(φ)
6513
+ new_pos[i, 1] = r * np.sin(polar_angle[i]) * np.sin(azimuth_angle[i]) # Y = r * sin(θ) * sin(φ)
6514
+ new_pos[i, 2] = r * np.cos(polar_angle[i]) # Z = r * cos(θ)
6515
+
6516
+ return new_pos
5906
6517
 
5907
6518
  if not Topology.IsInstance(graph, "Graph"):
5908
- print("Graph.Flatten - Error: The input graph is not a valid topologic graph. Returning None.")
6519
+ if not silent:
6520
+ print("Graph.Flatten - Error: The input graph is not a valid topologic graph. Returning None.")
5909
6521
  return None
5910
6522
 
5911
- if 'circ' in layout.lower():
5912
- new_graph = circle_layout(graph, radius=radius)
5913
- return new_graph
5914
- d = Graph.MeshData(graph)
5915
- vertices = d['vertices']
5916
- edges = d['edges']
5917
- v_dicts = d['vertexDictionaries']
5918
- e_dicts = d['edgeDictionaries']
5919
- vertices = Graph.Vertices(graph)
5920
- if rootVertex == None:
5921
- rootVertex, root_index = vertex_max_degree(graph, vertices)
6523
+ if 'circ' in shape.lower():
6524
+ return circle_layout_2d(graph, radius=size/2, sides=sides)
6525
+ elif 'lin' in shape.lower():
6526
+ return line_layout_2d(graph, length=size)
6527
+ elif 'grid' in shape.lower() and '2d' in shape.lower():
6528
+ return grid_layout_2d(graph, size=size)
6529
+ elif 'sphere' == shape.lower() and '3d' in shape.lower():
6530
+ return sphere_layout_3d(graph, radius=size/2)
6531
+ elif 'grid' in shape.lower() and '3d' in shape.lower():
6532
+ return grid_layout_3d(graph, size=size)
6533
+ elif 'cluster' in shape.lower() and '2d' in shape.lower():
6534
+ return cluster_layout_2d(graph, radius=size/2, key=key)
6535
+ elif 'cluster' in shape.lower() and '3d' in shape.lower():
6536
+ return cluster_layout_3d(graph, radius=size/2, key=key)
5922
6537
  else:
5923
- root_index = Vertex.Index(rootVertex, vertices, tolerance=tolerance)
5924
- if root_index == None:
5925
- root_index = 0
5926
- if 'rad' in layout.lower():
5927
- positions = radial_layout(edges, root_index=root_index)
5928
- elif 'spring' in layout.lower():
5929
- positions = spring_layout(edges, k=k, seed=seed, iterations=iterations)
5930
- elif 'tree' in layout.lower():
5931
- positions = tree_layout(edges, root_index=root_index)
5932
- else:
5933
- raise NotImplementedError(f"{layout} is not implemented yet. Please choose from ['radial', 'spring', 'tree']")
5934
- positions = positions.tolist()
5935
- positions = [[p[0], p[1], 0] for p in positions]
5936
- flat_graph = Graph.ByMeshData(positions, edges, v_dicts, e_dicts, tolerance=tolerance)
5937
- return flat_graph
6538
+ d = Graph.MeshData(graph)
6539
+ edges = d['edges']
6540
+ v_dicts = d['vertexDictionaries']
6541
+ e_dicts = d['edgeDictionaries']
6542
+ vertices = Graph.Vertices(graph)
6543
+ if rootVertex == None:
6544
+ rootVertex, root_index = vertex_max_degree(graph, vertices)
6545
+ else:
6546
+ root_index = Vertex.Index(rootVertex, vertices, tolerance=tolerance)
6547
+ if root_index == None:
6548
+ root_index = 0
6549
+ if 'rad' in shape.lower() and '2d' in shape.lower():
6550
+ positions = radial_layout_2d(edges, root_index=root_index)
6551
+ elif 'spherical' in shape.lower() and '3d' in shape.lower():
6552
+ positions = spherical_layout_3d(edges, root_index=root_index)
6553
+ elif 'spring' in shape.lower() and "3d" in shape.lower():
6554
+ positions = spring_layout_3d(edges, k=k, seed=seed, iterations=iterations)
6555
+ elif 'spring' in shape.lower() and '2d' in shape.lower():
6556
+ positions = spring_layout_2d(edges, k=k, seed=seed, iterations=iterations)
6557
+ elif 'tree' in shape.lower() and '2d' in shape.lower():
6558
+ positions = tree_layout_2d(edges, root_index=root_index)
6559
+ elif 'tree' in shape.lower() and '3d' in shape.lower():
6560
+ positions = tree_layout_3d(edges, root_index=root_index, base_radius=1.0, radius_factor=1.5)
6561
+ else:
6562
+ if not silent:
6563
+ print(f"{shape} is not implemented yet. Please choose from ['circle 2D', 'grid 2D', 'line 2D', 'radial 2D', 'spring 2D', 'tree 2D', 'grid 3D', 'sphere 3D', 'tree 3D']. Returning None.")
6564
+ return None
6565
+ positions = positions.tolist()
6566
+ if len(positions[0]) == 3:
6567
+ positions = [[p[0], p[1], p[2]] for p in positions]
6568
+ else:
6569
+ positions = [[p[0], p[1], 0] for p in positions]
6570
+ return Graph.ByMeshData(positions, edges, v_dicts, e_dicts, tolerance=tolerance)
5938
6571
 
5939
6572
  @staticmethod
5940
6573
  def GlobalClusteringCoefficient(graph):
topologicpy/Plotly.py CHANGED
@@ -415,23 +415,24 @@ class Plotly:
415
415
  e_cluster = Cluster.ByTopologies(vertices)
416
416
  geo = Topology.Geometry(e_cluster, mantissa=mantissa)
417
417
  vertices = geo['vertices']
418
- v_data = Plotly.DataByTopology(e_cluster,
419
- vertexColor=vertexColor,
420
- vertexColorKey=vertexColorKey,
421
- vertexSize=vertexSize,
422
- vertexSizeKey=vertexSizeKey,
423
- vertexLabelKey=vertexLabelKey,
424
- showVertexLabel=showVertexLabel,
425
- vertexGroupKey=vertexGroupKey,
426
- vertexMinGroup=vertexMinGroup,
427
- vertexMaxGroup=vertexMaxGroup,
428
- vertexGroups=vertexGroups,
429
- vertexLegendLabel=vertexLegendLabel,
430
- vertexLegendGroup=vertexLegendGroup,
431
- vertexLegendRank=vertexLegendRank,
432
- colorScale=colorScale)
433
-
434
- data += v_data
418
+ if len(vertices) > 0:
419
+ v_data = Plotly.DataByTopology(e_cluster,
420
+ vertexColor=vertexColor,
421
+ vertexColorKey=vertexColorKey,
422
+ vertexSize=vertexSize,
423
+ vertexSizeKey=vertexSizeKey,
424
+ vertexLabelKey=vertexLabelKey,
425
+ showVertexLabel=showVertexLabel,
426
+ vertexGroupKey=vertexGroupKey,
427
+ vertexMinGroup=vertexMinGroup,
428
+ vertexMaxGroup=vertexMaxGroup,
429
+ vertexGroups=vertexGroups,
430
+ vertexLegendLabel=vertexLegendLabel,
431
+ vertexLegendGroup=vertexLegendGroup,
432
+ vertexLegendRank=vertexLegendRank,
433
+ colorScale=colorScale)
434
+
435
+ data += v_data
435
436
 
436
437
  if showEdges:
437
438
  e_dictionaries = []
@@ -458,12 +459,12 @@ class Plotly:
458
459
  else:
459
460
  new_edges = edges
460
461
  e_dictionaries.append(d)
461
-
462
- e_cluster = Cluster.ByTopologies(new_edges)
463
- geo = Topology.Geometry(e_cluster, mantissa=mantissa)
464
- vertices = geo['vertices']
465
- edges = geo['edges']
466
- data.extend(Plotly.edgeData(vertices, edges, dictionaries=e_dictionaries, color=edgeColor, colorKey=edgeColorKey, width=edgeWidth, widthKey=edgeWidthKey, labelKey=edgeLabelKey, showEdgeLabel=showEdgeLabel, groupKey=edgeGroupKey, minGroup=edgeMinGroup, maxGroup=edgeMaxGroup, groups=edgeGroups, legendLabel=edgeLegendLabel, legendGroup=edgeLegendGroup, legendRank=edgeLegendRank, showLegend=showEdgeLegend, colorScale=colorScale))
462
+ if len(new_edges) > 0:
463
+ e_cluster = Cluster.ByTopologies(new_edges)
464
+ geo = Topology.Geometry(e_cluster, mantissa=mantissa)
465
+ vertices = geo['vertices']
466
+ edges = geo['edges']
467
+ data.extend(Plotly.edgeData(vertices, edges, dictionaries=e_dictionaries, color=edgeColor, colorKey=edgeColorKey, width=edgeWidth, widthKey=edgeWidthKey, labelKey=edgeLabelKey, showEdgeLabel=showEdgeLabel, groupKey=edgeGroupKey, minGroup=edgeMinGroup, maxGroup=edgeMaxGroup, groups=edgeGroups, legendLabel=edgeLegendLabel, legendGroup=edgeLegendGroup, legendRank=edgeLegendRank, showLegend=showEdgeLegend, colorScale=colorScale))
467
468
  return data
468
469
 
469
470
  @staticmethod
@@ -496,7 +497,7 @@ class Plotly:
496
497
  x.append(v[0])
497
498
  y.append(v[1])
498
499
  z.append(v[2])
499
- label = ""
500
+ label = " "
500
501
  group = None
501
502
  if len(dictionaries) > 0:
502
503
  d = dictionaries[m]
@@ -506,15 +507,13 @@ class Plotly:
506
507
  color = Color.AnyToHex(d_color)
507
508
  groupList.append(color)
508
509
  if not labelKey == None:
509
- label = str(Dictionary.ValueAtKey(d, key=labelKey)) or ""
510
+ label = str(Dictionary.ValueAtKey(d, key=labelKey)) or " "
510
511
  if not sizeKey == None:
511
512
  size = Dictionary.ValueAtKey(d, key=sizeKey) or size
512
513
  if not groupKey == None:
513
- group = Dictionary.ValueAtKey(d, key=groupKey) or ""
514
+ group = Dictionary.ValueAtKey(d, key=groupKey) or " "
514
515
  try:
515
- if group == "":
516
- #color = 'white'
517
- #groupList.append(Color.AnyToHex(color))
516
+ if group == " ":
518
517
  pass
519
518
  elif type(group) == int or type(group) == float:
520
519
  if group < minGroup:
@@ -571,8 +570,6 @@ class Plotly:
571
570
  from topologicpy.Color import Color
572
571
  from topologicpy.Dictionary import Dictionary
573
572
  from topologicpy.Helper import Helper
574
- for d in dictionaries[:30]:
575
- print(Dictionary.Keys(d), Dictionary.Values(d))
576
573
  traces = []
577
574
  x = []
578
575
  y = []
@@ -855,121 +852,6 @@ class Plotly:
855
852
  def closest_index(input_value, values):
856
853
  return int(min(range(len(values)), key=lambda i: abs(values[i] - input_value)))
857
854
 
858
- def edgeData_old(vertices, edges, dictionaries=None, color="black", colorKey=None, width=1, widthKey=None, labelKey=None, showEdgeLabel = False, groupKey=None, minGroup=None, maxGroup=None, groups=[], legendLabel="Topology Edges", legendGroup=2, legendRank=2, showLegend=True, colorScale="Viridis"):
859
- traces = []
860
- x = []
861
- y = []
862
- z = []
863
- labels = []
864
- groupList = []
865
- label = ""
866
- group = ""
867
-
868
- if showEdgeLabel == True:
869
- mode = "lines+text"
870
- else:
871
- mode = "lines"
872
-
873
- if showEdgeLabel == True:
874
- mode = "lines+text"
875
- else:
876
- mode = "lines"
877
- if groups:
878
- if len(groups) > 0:
879
- if type(groups[0]) == int or type(groups[0]) == float:
880
- if not minGroup:
881
- minGroup = min(groups)
882
- if not maxGroup:
883
- maxGroup = max(groups)
884
- else:
885
- minGroup = 0
886
- maxGroup = len(groups) - 1
887
- else:
888
- minGroup = 0
889
- maxGroup = 1
890
-
891
- if colorKey or widthKey or labelKey or groupKey:
892
- keys = [x for x in [colorKey, widthKey, labelKey, groupKey] if not x == None]
893
- temp_dict = Helper.ClusterByKeys(edges, dictionaries, keys, silent=False)
894
- dict_clusters = temp_dict["dictionaries"]
895
- elements_clusters = temp_dict['elements']
896
- for j, elements_cluster in enumerate(elements_clusters):
897
- d = dict_clusters[j][0] # All dicitonaries have same values in dictionaries, so take first one.
898
- if d:
899
- if not colorKey == None:
900
- d_color = Dictionary.ValueAtKey(d, key=colorKey) or color
901
- color = Color.AnyToHex(d_color)
902
- if not labelKey == None:
903
- label = str(Dictionary.ValueAtKey(d, key=labelKey)) or ""
904
- if not widthKey == None:
905
- width = Dictionary.ValueAtKey(d, key=edgeWidthKey) or width
906
- if not groupKey == None:
907
- group = Dictionary.ValueAtKey(d, key=groupKey)
908
- if not group == None:
909
- if type(group) == int or type(group) == float:
910
- if group < minGroup:
911
- group = minGroup
912
- if group > maxGroup:
913
- group = maxGroup
914
- d_color = Color.ByValueInRange(group, minValue=minGroup, maxValue=maxGroup, colorScale=colorScale)
915
- else:
916
- d_color = Color.ByValueInRange(groups.index(group), minValue=minGroup, maxValue=maxGroup, colorScale=colorScale)
917
- color = d_color
918
- x = []
919
- y = []
920
- z = []
921
- for e in elements_cluster:
922
- sv = vertices[e[0]]
923
- ev = vertices[e[1]]
924
- x+=[sv[0], ev[0], None] # x-coordinates of edge ends
925
- y+=[sv[1], ev[1], None] # y-coordinates of edge ends
926
- z+=[sv[2], ev[2], None] # z-coordinates of edge ends
927
- if showEdgeLabel == True:
928
- mode = "lines+text"
929
- else:
930
- mode = "lines"
931
- trace = go.Scatter3d(x=x,
932
- y=y,
933
- z=z,
934
- name=label,
935
- showlegend=showLegend,
936
- marker_size=0,
937
- mode=mode,
938
- line=dict(color=color, width=width),
939
- legendgroup=legendGroup,
940
- legendrank=legendRank,
941
- text=label,
942
- hoverinfo='text')
943
- traces.append(trace)
944
- else:
945
- x = []
946
- y = []
947
- z = []
948
- for e in edges:
949
- sv = vertices[e[0]]
950
- ev = vertices[e[1]]
951
- x+=[sv[0], ev[0], None] # x-coordinates of edge ends
952
- y+=[sv[1], ev[1], None] # y-coordinates of edge ends
953
- z+=[sv[2], ev[2], None] # z-coordinates of edge ends
954
- if showEdgeLabel == True:
955
- mode = "lines+text"
956
- else:
957
- mode = "lines"
958
- trace = go.Scatter3d(x=x,
959
- y=y,
960
- z=z,
961
- name=label,
962
- showlegend=showLegend,
963
- marker_size=0,
964
- mode=mode,
965
- line=dict(color=color, width=width),
966
- legendgroup=legendGroup,
967
- legendrank=legendRank,
968
- text=label,
969
- hoverinfo='text')
970
- traces.append(trace)
971
-
972
- return traces
973
855
 
974
856
  def faceData(vertices, faces, dictionaries=None, color="#FAFAFA", colorKey=None,
975
857
  opacity=0.5, labelKey=None, groupKey=None,
@@ -1152,7 +1034,8 @@ class Plotly:
1152
1034
  geo = Topology.Geometry(e_cluster, mantissa=mantissa)
1153
1035
  vertices = geo['vertices']
1154
1036
  edges = geo['edges']
1155
- data.extend(Plotly.edgeData(vertices, edges, dictionaries=e_dictionaries, color=edgeColor, colorKey=edgeColorKey, width=edgeWidth, widthKey=edgeWidthKey, labelKey=edgeLabelKey, showEdgeLabel=showEdgeLabel, groupKey=edgeGroupKey, minGroup=edgeMinGroup, maxGroup=edgeMaxGroup, groups=edgeGroups, legendLabel=edgeLegendLabel, legendGroup=edgeLegendGroup, legendRank=edgeLegendRank, showLegend=showEdgeLegend, colorScale=colorScale))
1037
+ if len(edges) > 0:
1038
+ data.extend(Plotly.edgeData(vertices, edges, dictionaries=e_dictionaries, color=edgeColor, colorKey=edgeColorKey, width=edgeWidth, widthKey=edgeWidthKey, labelKey=edgeLabelKey, showEdgeLabel=showEdgeLabel, groupKey=edgeGroupKey, minGroup=edgeMinGroup, maxGroup=edgeMaxGroup, groups=edgeGroups, legendLabel=edgeLegendLabel, legendGroup=edgeLegendGroup, legendRank=edgeLegendRank, showLegend=showEdgeLegend, colorScale=colorScale))
1156
1039
 
1157
1040
  if showFaces and Topology.Type(topology) >= Topology.TypeID("Face"):
1158
1041
  if not faceColorKey == None:
@@ -1185,7 +1068,8 @@ class Plotly:
1185
1068
  geo = Topology.Geometry(f_cluster, mantissa=mantissa)
1186
1069
  vertices = geo['vertices']
1187
1070
  faces = geo['faces']
1188
- data.append(faceData(vertices, faces, dictionaries=f_dictionaries, color=faceColor, colorKey=faceColorKey, opacity=faceOpacity, labelKey=faceLabelKey, groupKey=faceGroupKey, minGroup=faceMinGroup, maxGroup=faceMaxGroup, groups=faceGroups, legendLabel=faceLegendLabel, legendGroup=faceLegendGroup, legendRank=faceLegendRank, showLegend=showFaceLegend, intensities=intensityList, colorScale=colorScale))
1071
+ if len(faces) > 0:
1072
+ data.append(faceData(vertices, faces, dictionaries=f_dictionaries, color=faceColor, colorKey=faceColorKey, opacity=faceOpacity, labelKey=faceLabelKey, groupKey=faceGroupKey, minGroup=faceMinGroup, maxGroup=faceMaxGroup, groups=faceGroups, legendLabel=faceLegendLabel, legendGroup=faceLegendGroup, legendRank=faceLegendRank, showLegend=showFaceLegend, intensities=intensityList, colorScale=colorScale))
1189
1073
  return data
1190
1074
 
1191
1075
  @staticmethod
topologicpy/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = '0.7.72'
1
+ __version__ = '0.7.74'
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: topologicpy
3
- Version: 0.7.72
3
+ Version: 0.7.74
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
@@ -2,7 +2,7 @@ topologicpy/ANN.py,sha256=XAuUjNvDRK1hhXfo82S-zXmnAPZGEdHJMRdfpu0aJ8I,47901
2
2
  topologicpy/Aperture.py,sha256=p9pUzTQSBWoUaDiug1V1R1hnEIEwYSXFg2t7iRAmNRY,2723
3
3
  topologicpy/BVH.py,sha256=mKVCAu9K8qzcWXtPDVH5usXZV1DNNNJl4n3rU5Lh1ZM,12931
4
4
  topologicpy/Cell.py,sha256=2izd-YGqy897_JHHgrGlIo5WwUeEIWVD3KspV1z_sj8,107860
5
- topologicpy/CellComplex.py,sha256=x3Exodx2PeXynnnqzb3ZLKtlSdey8KaaiKpkcx8HoCc,48207
5
+ topologicpy/CellComplex.py,sha256=5PtnRrDx_3zmtt4aTosxK9XBzZtayzaOC50pkJiIlFY,51170
6
6
  topologicpy/Cluster.py,sha256=51q5G1L5xAzRMfVU8YBXhq0g3g2X9aVNcahU-vYZRrI,55672
7
7
  topologicpy/Color.py,sha256=wPhA7rLr9BTZsWYUUVnQpbmL5ZMkGlDSsa8f3S5B-d4,20250
8
8
  topologicpy/Context.py,sha256=ppApYKngZZCQBFWaxIMi2z2dokY23c935IDCBosxDAE,3055
@@ -11,13 +11,13 @@ topologicpy/Dictionary.py,sha256=0AsGoz48pGTye_F4KcJopNjD9STeQ50LHc6PPvERFaA,319
11
11
  topologicpy/Edge.py,sha256=9u9SdUxuenLUIK26xwFvPoYV34p0dCfXmHHBxdgvAdM,67164
12
12
  topologicpy/EnergyModel.py,sha256=AqTtmXE35SxvRXhG3vYAQd7GQDW-6HtjYPHua6ME4Eg,53762
13
13
  topologicpy/Face.py,sha256=q7x6auTju6IS3mdOhhXZdU3rqKSuJCE-5EOfxofDDMI,124348
14
- topologicpy/Graph.py,sha256=M4m0URB812Fb8ISdiSftaZ-XJv8tOHY9eeba43-C6W0,386261
14
+ topologicpy/Graph.py,sha256=BuSg8ilbZd2Qqoenmtw5FrkSe1fDLFS6jshljvq4dOs,416822
15
15
  topologicpy/Grid.py,sha256=9N6PE84qCm40TRi2WtlVZSBwXXr47zHpscEpZHg_JW4,18205
16
16
  topologicpy/Helper.py,sha256=Sv35czP_j0oLDeJcN8usswUm4U3auiK1LQ_Z_HBvxxg,21716
17
17
  topologicpy/Honeybee.py,sha256=HfTaEV1R8K1xOVQQy9sBOhBTF_ap8A2RxZOYhirp_Mw,21835
18
18
  topologicpy/Matrix.py,sha256=umgR7An919-wGInXJ1wpqnoQ2jCPdyMe2rcWTZ16upk,8079
19
19
  topologicpy/Neo4j.py,sha256=t52hgE9cVsqkGc7m7fjRsLnyfRHakVHwdvF4ms7ow78,22342
20
- topologicpy/Plotly.py,sha256=KU76pKFCfPUTmWMqMeyzbRbyri5VEPASKQZ9DQX4Evc,118323
20
+ topologicpy/Plotly.py,sha256=VNUHRUNu4NUmf3HScQRml3TcwHe_ZoqXjqwttGSuU88,112642
21
21
  topologicpy/Polyskel.py,sha256=EFsuh2EwQJGPLiFUjvtXmAwdX-A4r_DxP5hF7Qd3PaU,19829
22
22
  topologicpy/PyG.py,sha256=LU9LCCzjxGPUM31qbaJXZsTvniTtgugxJY7y612t4A4,109757
23
23
  topologicpy/Shell.py,sha256=8OJjlWk9eCZ3uGOTht6ZVrcMczCafw-YWoDGueaz7eg,87673
@@ -28,9 +28,9 @@ topologicpy/Vector.py,sha256=A1g83zDHep58iVPY8WQ8iHNrSOfGWFEzvVeDuMnjDNY,33078
28
28
  topologicpy/Vertex.py,sha256=ZS6xK89JKokBKc0W8frdRhhuzR8c-dI1TTLt7pTf1iA,71032
29
29
  topologicpy/Wire.py,sha256=eVet2OToVsXi9AkDYo35LpfMPqJ6aKGD6QkiU4-Jvs8,182271
30
30
  topologicpy/__init__.py,sha256=vlPCanUbxe5NifC4pHcnhSzkmmYcs_UrZrTlVMsxcFs,928
31
- topologicpy/version.py,sha256=aAQjVNTNHAmKloX4yKWiynz5GQOXtst-3Q4eQldJrXM,23
32
- topologicpy-0.7.72.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
- topologicpy-0.7.72.dist-info/METADATA,sha256=Tj5f47D3-QD3OHRQDGyO-_BByNcAXuEWIEsdT6Nuob0,10493
34
- topologicpy-0.7.72.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
35
- topologicpy-0.7.72.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
- topologicpy-0.7.72.dist-info/RECORD,,
31
+ topologicpy/version.py,sha256=_XwNUtWN5b4vRt6q08wKnk-7iLHDZXyRKXWBhgnWyP4,23
32
+ topologicpy-0.7.74.dist-info/LICENSE,sha256=FK0vJ73LuE8PYJAn7LutsReWR47-Ooovw2dnRe5yV6Q,681
33
+ topologicpy-0.7.74.dist-info/METADATA,sha256=QQ-v5awljewQVDPhAMmdZdVtUcKzJh4LRiYGoCePtmk,10493
34
+ topologicpy-0.7.74.dist-info/WHEEL,sha256=R06PA3UVYHThwHvxuRWMqaGcr-PuniXahwjmQRFMEkY,91
35
+ topologicpy-0.7.74.dist-info/top_level.txt,sha256=J30bDzW92Ob7hw3zA8V34Jlp-vvsfIkGzkr8sqvb4Uw,12
36
+ topologicpy-0.7.74.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.5.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5