From d11f6035aef51a7837b2801cbe72c6506c139eed Mon Sep 17 00:00:00 2001 From: Wassim Jabi Date: Thu, 19 Dec 2024 08:16:18 +0000 Subject: [PATCH] Added Graph.ConnectedComponents. Many bug fixes. Bumped to v0.7.88 --- notebooks/GraphByIFCPath.ipynb | 299 +++++++++++++++++++++++++------- notebooks/Isovist_Metrics.ipynb | 3 +- src/topologicpy/Cell.py | 10 +- src/topologicpy/Face.py | 59 +++++-- src/topologicpy/Graph.py | 240 +++++++++++++++++++------ src/topologicpy/Topology.py | 35 ++-- src/topologicpy/version.py | 2 +- 7 files changed, 497 insertions(+), 151 deletions(-) diff --git a/notebooks/GraphByIFCPath.ipynb b/notebooks/GraphByIFCPath.ipynb index c27cfd5..2a60494 100644 --- a/notebooks/GraphByIFCPath.ipynb +++ b/notebooks/GraphByIFCPath.ipynb @@ -30,11 +30,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "The version that you are using (0.7.86) is the latest version available on PyPI.\n" + "The version that you are using (0.7.88) is NEWER than the latest version (0.7.87) available from PyPI.\n" ] } ], "source": [ + "from topologicpy.Vertex import Vertex\n", "from topologicpy.CellComplex import CellComplex\n", "from topologicpy.Topology import Topology\n", "from topologicpy.Dictionary import Dictionary\n", @@ -71,28 +72,48 @@ "execution_count": 5, "id": "b49221ef", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Done\n" + ] + } + ], "source": [ - "# Get the graph from the IFC path\n", - "graph = Graph.ByIFCPath(ifc_file_path,\n", + "# Create a graph from the IFC path\n", + "graph1 = Graph.ByIFCPath(ifc_file_path,\n", " includeTypes= include_types,\n", " transferDictionaries=True,\n", + " useInternalVertex=True,\n", " storeBREP=True,\n", " removeCoplanarFaces=True)\n", "\n", - "d = Dictionary.ByKeyValue(\"IFC_name\", \"Graph\")\n", - "graph = Graph.SetDictionary(graph, d)\n", "# Extract the topologies from the vertices of the graph\n", - "vertices = Graph.Vertices(graph)\n", "topologies = []\n", - "for v in vertices:\n", + "rogue_vertices = [] # These are rogue vertices that have no topology associated with them.\n", + "for v in Graph.Vertices(graph1):\n", " d = Topology.Dictionary(v)\n", " brep_string = Dictionary.ValueAtKey(d, \"brep\")\n", " if brep_string:\n", " topology = Topology.ByBREPString(brep_string)\n", - " if topology:\n", + " if Topology.IsInstance(topology, \"Topology\"):\n", " topology = Topology.SetDictionary(topology, d)\n", - " topologies.append(topology)" + " topologies.append(topology)\n", + " else:\n", + " rogue_vertices.append(v)\n", + " else:\n", + " rogue_vertices.append(v)\n", + "\n", + "# Remove rogue vertices from the graph\n", + "for rogue_vertex in rogue_vertices:\n", + " graph1 = Graph.RemoveVertex(graph1, rogue_vertex)\n", + "\n", + "# Give the graph a fake IFC name to be displayed in the legend.\n", + "d = Dictionary.ByKeyValue(\"IFC_name\", \"Graph\")\n", + "graph1 = Graph.SetDictionary(graph1, d)\n", + "print(\"Done\")" ] }, { @@ -100,6 +121,22 @@ "execution_count": 6, "id": "56fa6539", "metadata": {}, + "outputs": [], + "source": [ + "centralities = Graph.ClosenessCentrality(graph1, silent=False)\n", + "vertices = Graph.Vertices(graph1)\n", + "for v in vertices:\n", + " d = Topology.Dictionary(v)\n", + " c = Dictionary.ValueAtKey(d,\"closeness_centrality\")\n", + " d = Dictionary.SetValueAtKey(d, \"closeness_centrality\", c*20+4)\n", + " v = Topology.SetDictionary(v, d)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "a5c3aaad", + "metadata": {}, "outputs": [ { "data": { @@ -133,9 +170,9 @@ { "data": { "text/html": [ - "
\n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "for t in topologies[:5]:\n", - " d = Topology.Dictionary(t)\n", - " print(Dictionary.Keys(d), Dictionary.Values(d))" + "# Import TopologicPy modules\n", + "import sys\n", + "sys.path.append(\"C:/Users/sarwj/OneDrive - Cardiff University/Documents/GitHub/topologicpy/src\")\n", + "from topologicpy.Vertex import Vertex\n", + "from topologicpy.CellComplex import CellComplex\n", + "from topologicpy.Topology import Topology\n", + "from topologicpy.Dictionary import Dictionary\n", + "from topologicpy.Graph import Graph\n", + "from topologicpy.Helper import Helper\n", + "from topologicpy.Cluster import Cluster\n", + "\n", + "\n", + "components = Graph.ConnectedComponents(graph1)\n", + "Topology.Show(components[1],\n", + " nameKey=\"IFC_name\",\n", + " sagitta= 0.05,\n", + " absolute=False,\n", + " faceOpacity=0.1,\n", + " vertexSizeKey=\"closeness_centrality\",\n", + " vertexLabelKey=\"IFC_name\",\n", + " vertexGroupKey=\"IFC_type\",\n", + " vertexGroups=[\"Unknown\", \"IfcSpace\", \"IfcSlab\", \"IfcRoof\", \"IfcWall\", \"IfcWallStandardCase\", \"IfcDoor\", \"IfcWindow\"],\n", + " showVertexLegend = False,\n", + " showEdgeLegend = False,\n", + " showFaceLegend = False,\n", + " backgroundColor=\"white\", width=1024, height=900)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "921eb112", + "execution_count": 9, + "id": "ca51be9a", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[]\n" + ] + }, + { + "data": { + "text/html": [ + " \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "# Let’s Build the Cell Complex\n", - "# Make sure there are only “cells” in the topologies\n", - "cells = [t for t in topologies if Topology.IsInstance(t, \"Cell\")]\n", - "# Build the cellComplex from the cells list\n", - "cellComplex = CellComplex.ByCells(cells)\n", - "\n", - "# Get cell selectors to transfer dictionaries to the cells of the cellComplex\n", - "cell_selectors = Graph.Vertices(graph)\n", - "\n", - "# Transfer cell dictionaries to the cells of the cellComplex\n", - "cellComplex = Topology.TransferDictionariesBySelectors(cellComplex, selectors=vertices, tranCells=True)\n", - "\n", - "# Let’s transfer the dictionary of each cell to the faces of that cell using face selectors.\n", - "face_selectors = []\n", - "for cell in cells:\n", - " faces = Topology.Faces(cell)\n", - " dictionary = Topology.Dictionary(cell)\n", - " for face in faces:\n", - " s = None\n", - " try:\n", - " s = Topology.InternalVertex(face) # If this fails for some reason, try using s = Topology.Centroid(face)\n", - " except:\n", - " s = Topology.Centroid(face)\n", - " if not s == None:\n", - " s = Topology.SetDictionary(s, dictionary)\n", - " face_selectors.append(s)\n", - "\n", - "# Transfer face dictionaries to the faces of the cellComplex\n", - "cellComplex = Topology.TransferDictionariesBySelectors(cellComplex, selectors=face_selectors, tranFaces=True)\n", - "\n" + "vertices = Graph.Vertices(components[3])\n", + "print(vertices)\n", + "Topology.Show(components[3],\n", + " nameKey=\"IFC_name\",\n", + " sagitta= 0.05,\n", + " absolute=False,\n", + " faceOpacity=0.1,\n", + " vertexSizeKey=\"closeness_centrality\",\n", + " vertexLabelKey=\"IFC_name\",\n", + " vertexGroupKey=\"IFC_type\",\n", + " vertexGroups=[\"Unknown\", \"IfcSpace\", \"IfcSlab\", \"IfcRoof\", \"IfcWall\", \"IfcWallStandardCase\", \"IfcDoor\", \"IfcWindow\"],\n", + " showVertexLegend = False,\n", + " showEdgeLegend = False,\n", + " showFaceLegend = False,\n", + " backgroundColor=\"white\", width=1024, height=900)" ] } ], diff --git a/notebooks/Isovist_Metrics.ipynb b/notebooks/Isovist_Metrics.ipynb index 90082e3..cea1b5d 100644 --- a/notebooks/Isovist_Metrics.ipynb +++ b/notebooks/Isovist_Metrics.ipynb @@ -318,7 +318,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": {}, "outputs": [ { @@ -395,7 +395,6 @@ " for edge in edges:\n", " d = Topology.Dictionary(edge)\n", " d = Dictionary.SetValueAtKey(d, \"edgeWidth\", 0.001)\n", - "\n", " edge = Topology.SetDictionary(edge, d)\n", "Topology.Show(r, r1, r2, triangles,v, vertexSizeKey=\"vertexSize\", edgeColor=\"black\",\n", " edgeColorKey=\"edgeColor\", edgeWidth=1, edgeWidthKey=\"edgeWidth\",\n", diff --git a/src/topologicpy/Cell.py b/src/topologicpy/Cell.py index 0e0cf63..e212386 100644 --- a/src/topologicpy/Cell.py +++ b/src/topologicpy/Cell.py @@ -1471,7 +1471,7 @@ def InternalBoundaries(cell) -> list: return shells @staticmethod - def InternalVertex(cell, tolerance: float = 0.0001): + def InternalVertex(cell, tolerance: float = 0.0001, silent: bool = False): """ Creates a vertex that is guaranteed to be inside the input cell. @@ -1481,6 +1481,8 @@ def InternalVertex(cell, tolerance: float = 0.0001): The input cell. tolerance : float , optional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- @@ -1491,12 +1493,14 @@ def InternalVertex(cell, tolerance: float = 0.0001): from topologicpy.Topology import Topology if not Topology.IsInstance(cell, "Cell"): - print("Cell.InternalVertex - Error: The input cell parameter is not a valid topologic cell. Returning None.") + if not silent: + print("Cell.InternalVertex - Error: The input cell parameter is not a valid topologic cell. Returning None.") return None try: return topologic.CellUtility.InternalVertex(cell, tolerance) # Hook to Core except: - print("Cell.InternalVertex - Error: Could not create an internal vertex. Returning None.") + if not silent: + print("Cell.InternalVertex - Error: Could not create an internal vertex. Returning None.") return None @staticmethod diff --git a/src/topologicpy/Face.py b/src/topologicpy/Face.py index 1f23494..a2e19e4 100644 --- a/src/topologicpy/Face.py +++ b/src/topologicpy/Face.py @@ -1518,7 +1518,7 @@ def InternalBoundaries(face) -> list: return list(wires) @staticmethod - def InternalVertex(face, tolerance: float = 0.0001): + def InternalVertex(face, tolerance: float = 0.0001, silent: bool = False): """ Creates a vertex guaranteed to be inside the input face. @@ -1528,6 +1528,8 @@ def InternalVertex(face, tolerance: float = 0.0001): The input face. tolerance : float , optional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- @@ -1535,22 +1537,53 @@ def InternalVertex(face, tolerance: float = 0.0001): The created vertex. """ + def get_uv_radially(): + """ + Generate the points of a grid with a given size n, sorted radially from the center to the periphery. + n should be an odd number, ensuring that there's a center point (0, 0). + + Args: + n (int): The size of the grid. It should be odd for a clear center point. + + Returns: + list: A list of tuples (x, y) sorted by radial distance from the center (0, 0). + """ + import math + + points = [] + n = 100 + # Iterate over the grid, ranging from -n//2 to n//2 + for x in range(-n//2, n//2 + 1): + for y in range(-n//2, n//2 + 1): + points.append((x, y)) + + # Sort points by their Euclidean distance from the center (0, 0) + points.sort(key=lambda point: math.sqrt(point[0]**2 + point[1]**2)) + return_points = [] + for p in points: + new_p = ((p[0]+50)*0.01, (p[1]+50)*0.01) + return_points.append(new_p) + return return_points + from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not Topology.IsInstance(face, "Face"): return None - v = Topology.Centroid(face) - if Vertex.IsInternal(v, face, tolerance=tolerance): - return v - l = [0.4,0.6,0.3,0.7,0.2,0.8,0.1,0.9] - for u in l: - for v in l: - v = Face.VertexByParameters(face, u, v) - if Vertex.IsInternal(v, face, tolerance=tolerance): - return v - v = topologic.FaceUtility.InternalVertex(face, tolerance) # Hook to Core - return v + vert = Topology.Centroid(face) + if Vertex.IsInternal(vert, face, tolerance=tolerance): + return vert + uv_list = get_uv_radially() + for uv in uv_list: + u, v = uv + vert = Face.VertexByParameters(face, u, v) + if Vertex.IsInternal(vert, face, tolerance=tolerance): + return vert + if not silent: + print("Face.InternalVertex - Warning: Could not find an internal vertex. Returning the first vertex of the face.") + vert = Topology.Vertices(face)[0] + #v = topologic.FaceUtility.InternalVertex(face, tolerance) # Hook to Core + return vert @staticmethod def Invert(face, tolerance: float = 0.0001): @@ -2365,6 +2398,8 @@ def NormalEdge(face, length: float = 1.0, tolerance: float = 0.0001, silent: boo The desired length of the normal edge. The default is 1. tolerance : float , optional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- diff --git a/src/topologicpy/Graph.py b/src/topologicpy/Graph.py index 84826e9..0708777 100644 --- a/src/topologicpy/Graph.py +++ b/src/topologicpy/Graph.py @@ -2160,9 +2160,14 @@ def verticesByCoordinates(x_coords, y_coords): return {'graphs':graphs, 'labels':labels} @staticmethod - def ByIFCFile(file, includeTypes: list = [], excludeTypes: list = [], - includeRels: list = [], excludeRels: list = [], - transferDictionaries: bool = False, storeBREP: bool = False, + def ByIFCFile(file, + includeTypes: list = [], + excludeTypes: list = [], + includeRels: list = [], + excludeRels: list = [], + transferDictionaries: bool = False, + useInternalVertex: bool = False, + storeBREP: bool = False, removeCoplanarFaces: bool = False, xMin: float = -0.5, yMin: float = -0.5, zMin: float = -0.5, xMax: float = 0.5, yMax: float = 0.5, zMax: float = 0.5, @@ -2184,6 +2189,8 @@ def ByIFCFile(file, includeTypes: list = [], excludeTypes: list = [], A list of IFC relationship types to exclude from the graph. The default is [] which mean no relationship type is excluded. transferDictionaries : bool , optional If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False. + useInternalVertex : bool , optional + If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. The default is False. storeBREP : bool , optional If set to True, store the BRep of the subtopology in its representative vertex. The default is False. removeCoplanarFaces : bool , optional @@ -2466,7 +2473,7 @@ def vertexByIFCObject(ifc_object, object_types, restrict=False): pset_python_dict = get_psets(ifc_object) pset_dict = Dictionary.ByPythonDictionary(pset_python_dict) topology_dict = Dictionary.ByMergedDictionaries([topology_dict, pset_dict]) - if storeBREP == True: + if storeBREP == True or useInternalVertex == True: shape_topology = None if hasattr(ifc_object, "Representation") and ifc_object.Representation: for rep in ifc_object.Representation.Representations: @@ -2484,8 +2491,10 @@ def vertexByIFCObject(ifc_object, object_types, restrict=False): if not shape_topology == None: if removeCoplanarFaces == True: shape_topology = Topology.RemoveCoplanarFaces(shape_topology, epsilon=0.0001) - if not shape_topology == None: + if not shape_topology == None and storeBREP: topology_dict = Dictionary.SetValuesAtKeys(topology_dict, ["brep", "brepType", "brepTypeString"], [Topology.BREPString(shape_topology), Topology.Type(shape_topology), Topology.TypeAsString(shape_topology)]) + if not shape_topology == None and useInternalVertex == True: + centroid = Topology.InternalVertex(shape_topology) centroid = Topology.SetDictionary(centroid, topology_dict) return centroid return None @@ -2571,7 +2580,16 @@ def edgesByIFCRelationships(ifc_relationships, ifc_types, vertices): return g @staticmethod - def ByIFCPath(path, includeTypes=[], excludeTypes=[], includeRels=[], excludeRels=[], transferDictionaries=False, storeBREP=False, removeCoplanarFaces=False, xMin=-0.5, yMin=-0.5, zMin=-0.5, xMax=0.5, yMax=0.5, zMax=0.5): + def ByIFCPath(path, + includeTypes=[], + excludeTypes=[], + includeRels=[], + excludeRels=[], + transferDictionaries=False, + useInternalVertex=False, + storeBREP=False, + removeCoplanarFaces=False, + xMin=-0.5, yMin=-0.5, zMin=-0.5, xMax=0.5, yMax=0.5, zMax=0.5): """ Create a Graph from an IFC path. This code is partially based on code from Bruno Postle. @@ -2589,6 +2607,8 @@ def ByIFCPath(path, includeTypes=[], excludeTypes=[], includeRels=[], excludeRel A list of IFC relationship types to exclude from the graph. The default is [] which mean no relationship type is excluded. transferDictionaries : bool , optional If set to True, the dictionaries from the IFC file will be transferred to the topology. Otherwise, they won't. The default is False. + useInternalVertex : bool , optional + If set to True, use an internal vertex to represent the subtopology. Otherwise, use its centroid. The default is False. storeBREP : bool , optional If set to True, store the BRep of the subtopology in its representative vertex. The default is False. removeCoplanarFaces : bool , optional @@ -2647,6 +2667,7 @@ def ByIFCPath(path, includeTypes=[], excludeTypes=[], includeRels=[], excludeRel includeRels=includeRels, excludeRels=excludeRels, transferDictionaries=transferDictionaries, + useInternalVertex=useInternalVertex, storeBREP=storeBREP, removeCoplanarFaces=removeCoplanarFaces, xMin=xMin, yMin=yMin, zMin=zMin, xMax=xMax, yMax=yMax, zMax=zMax) @@ -4081,6 +4102,79 @@ def graph_coloring(graph, m, colors): v = Topology.SetDictionary(v, d) return graph + @staticmethod + def ConnectedComponents(graph, tolerance: float = 0.0001, silent: bool = False): + """ + Returns the connected components (islands) of the input graph. + + Parameters + ---------- + graph : topologic_core.Graph + The input graph. + tolerance : float , optional + The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. + + Returns + ------- + list + The list of connected components (island graphs). + The list is sorted by the number of vertices in each component (from highest to lowest). + + """ + def find_connected_components(adjacency_dict): + visited = set() + components = [] + + for vertex_id in adjacency_dict: + if vertex_id not in visited: + # Perform DFS using a stack + stack = [vertex_id] + current_island = set() + + while stack: + current = stack.pop() + if current not in visited: + visited.add(current) + current_island.add(current) + stack.extend(set(adjacency_dict[current]) - visited) + + components.append(current_island) + + return components + + from topologicpy.Topology import Topology + from topologicpy.Dictionary import Dictionary + from topologicpy.Helper import Helper + + if not Topology.IsInstance(graph, "Graph"): + if not silent: + print("Graph.ConnectedComponents - Error: The input graph is not a valid graph. Returning None.") + return None + + labelKey = "__label__" + lengths = [] #List of lengths to sort the list of components by number of their vertices + vertices = Graph.Vertices(graph) + g_dict = Graph.AdjacencyDictionary(graph, vertexLabelKey=labelKey) + components = find_connected_components(g_dict) + return_components = [] + for component in components: + i_verts = [] + for v in component: + vert = Topology.Filter(vertices, searchType="equal to", key=labelKey, value=v)['filtered'][0] + d = Topology.Dictionary(vert) + d = Dictionary.RemoveKey(d, labelKey) + vert = Topology.SetDictionary(vert, d) + i_verts.append(vert) + i_edges = Graph.Edges(graph, i_verts) + lengths.append(len(i_verts)) + g_component = Graph.ByVerticesEdges(i_verts, i_edges) + return_components.append(g_component) + return_components = Helper.Sort(return_components, lengths) + return_components.reverse() + return return_components + @staticmethod def ContractEdge(graph, edge, vertex=None, tolerance=0.0001): """ @@ -4184,7 +4278,7 @@ def OppositeVertex(edge, vertex, tolerance=0.0001): return graph @staticmethod - def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", mantissa: int = 6, tolerance = 0.0001): + def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", mantissa: int = 6, tolerance = 0.0001, silent = False): """ Return the closeness centrality measure of the input list of vertices within the input graph. The order of the returned list is the same as the order of the input list of vertices. If no vertices are specified, the closeness centrality of all the vertices in the input graph is computed. See https://en.wikipedia.org/wiki/Closeness_centrality. @@ -4200,6 +4294,8 @@ def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", The desired length of the mantissa. The default is 6. tolerance : float , optional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- @@ -4207,56 +4303,68 @@ def ClosenessCentrality(graph, vertices=None, key: str = "closeness_centrality", The closeness centrality of the input list of vertices within the input graph. The values are in the range 0 to 1. """ + + def closeness_centrality(g): + """ + Computes the closeness centrality for each vertex in the graph. + + Parameters: + graph (dict): A dictionary representing the graph where keys are vertices and + values are lists of neighboring vertices. + + Returns: + dict: A dictionary where keys are vertices and values are their closeness centrality. + """ + keys = list(g.keys()) + N = len(keys) + + centralities = [] + for v in keys: + total_distance = 0 + reachable_count = 0 + + for u in keys: + if v != u: + distance = Graph._topological_distance(g, v, u) + if distance != None: + total_distance += distance + reachable_count += 1 + + if reachable_count > 0: # Avoid division by zero + centrality = (reachable_count / total_distance) + else: + centrality = 0.0 # Isolated vertex + + centralities.append(centrality) + return centralities + + from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology from topologicpy.Dictionary import Dictionary + from topologicpy.Helper import Helper if not Topology.IsInstance(graph, "Graph"): - print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.") + if not silent: + print("Graph.ClosenessCentrality - Error: The input graph is not a valid graph. Returning None.") return None + g = Graph.AdjacencyDictionary(graph) + centralities = closeness_centrality(g) graphVertices = Graph.Vertices(graph) - if not isinstance(vertices, list): - vertices = graphVertices + if vertices == None: + for i, v in enumerate(graphVertices): + d = Topology.Dictionary(v) + d = Dictionary.SetValueAtKey(d, key, centralities[i]) + v = Topology.SetDictionary(v, d) + return centralities else: - vertices = [v for v in vertices if Topology.IsInstance(v, "Vertex")] - if len(vertices) < 1: - print("Graph.ClosenessCentrality - Error: The input list of vertices does not contain any valid vertices. Returning None.") - return None - n = len(graphVertices) - - scores = [] - try: - for va in tqdm(vertices, desc="Computing Closeness Centrality", leave=False): - top_dist = 0 - for vb in graphVertices: - if Topology.IsSame(va, vb): - d = 0 - else: - d = Graph.TopologicalDistance(graph, va, vb, tolerance=tolerance) - top_dist += d - if top_dist == 0: - print("Graph.ClosenessCentrality - Warning: Topological Distance is Zero.") - scores.append(0) - else: - scores.append(round((n-1)/top_dist, mantissa)) - except: - print("Graph.ClosenessCentrality - Warning: Could not use tqdm.") - for va in vertices: - top_dist = 0 - for vb in graphVertices: - if Topology.IsSame(va, vb): - d = 0 - else: - d = Graph.TopologicalDistance(graph, va, vb, tolerance=tolerance) - top_dist += d - if top_dist == 0: - scores.append(0) - else: - scores.append(round((n-1)/top_dist, mantissa)) - for i, v in enumerate(vertices): - d = Topology.Dictionary(v) - d = Dictionary.SetValueAtKey(d, key, scores[i]) - v = Topology.SetDictionary(v, d) - return scores + return_centralities = [] + for v in vertices: + i = Vertex.Index(v, graphVertices) + d = Topology.Dictionary(v) + d = Dictionary.SetValueAtKey(d, key, centralities[i]) + v = Topology.SetDictionary(v, d) + return_centralities.append(centralities[i]) + return centralities @staticmethod def Connect(graph, verticesA, verticesB, tolerance=0.0001): @@ -8880,6 +8988,27 @@ def Size(graph): return len(Graph.Edges(graph)) @staticmethod + def _topological_distance(g, start, target): + from collections import deque + if start == target: + return 0 + visited = set() + queue = deque([(start, 0)]) # Each element is a tuple (vertex, distance) + + while queue: + current, distance = queue.popleft() + if current in visited: + continue + + visited.add(current) + for neighbor in g.get(current, []): + if neighbor == target: + return distance + 1 + if neighbor not in visited: + queue.append((neighbor, distance + 1)) + + return None # Target not reachable + @staticmethod def TopologicalDistance(graph, vertexA, vertexB, tolerance=0.0001): """ Returns the topological distance between the input vertices. See https://en.wikipedia.org/wiki/Distance_(graph_theory). @@ -8901,6 +9030,8 @@ def TopologicalDistance(graph, vertexA, vertexB, tolerance=0.0001): The topological distance between the input vertices. """ + + from topologicpy.Vertex import Vertex from topologicpy.Topology import Topology if not Topology.IsInstance(graph, "Graph"): @@ -8912,7 +9043,14 @@ def TopologicalDistance(graph, vertexA, vertexB, tolerance=0.0001): if not Topology.IsInstance(vertexB, "Vertex"): print("Graph.TopologicalDistance - Error: The input vertexB is not a valid vertex. Returning None.") return None - return graph.TopologicalDistance(vertexA, vertexB, tolerance) # Hook to Core + vertices = Graph.Vertices(graph) + g = Graph.AdjacencyDictionary(graph) + keys = list(g.keys()) + index_a = Vertex.Index(vertexA, vertices, tolerance=tolerance) + start = keys[index_a] + index_b = Vertex.Index(vertexB, vertices, tolerance=tolerance) + target = keys[index_b] + return Graph._topological_distance(g, start, target) # Hook to Core @staticmethod def Topology(graph): diff --git a/src/topologicpy/Topology.py b/src/topologicpy/Topology.py index 61705c7..f64c037 100644 --- a/src/topologicpy/Topology.py +++ b/src/topologicpy/Topology.py @@ -6240,7 +6240,7 @@ def HighestType(topology): return Topology.Type(topology) @staticmethod - def _InternalVertex(topology, tolerance: float = 0.0001): + def _InternalVertex(topology, tolerance: float = 0.0001, silent: bool = False): """ Returns a vertex guaranteed to be inside the input topology. @@ -6250,6 +6250,8 @@ def _InternalVertex(topology, tolerance: float = 0.0001): The input topology. tolerance : float , ptional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- @@ -6271,20 +6273,20 @@ def _InternalVertex(topology, tolerance: float = 0.0001): top = Topology.Copy(topology) if Topology.IsInstance(top, "CellComplex"): #CellComplex tempCell = Topology.Cells(top)[0] - vst = Cell.InternalVertex(tempCell, tolerance=tolerance) + vst = Cell.InternalVertex(tempCell, tolerance=tolerance, silent=silent) elif Topology.IsInstance(top, "Cell"): #Cell - vst = Cell.InternalVertex(top, tolerance=tolerance) + vst = Cell.InternalVertex(top, tolerance=tolerance, silent=silent) elif Topology.IsInstance(top, "Shell"): #Shell tempFace = Topology.Faces(top)[0] - vst = Face.InternalVertex(tempFace, tolerance=tolerance) + vst = Face.InternalVertex(tempFace, tolerance=tolerance, silent=silent) elif Topology.IsInstance(top, "Face"): #Face - vst = Face.InternalVertex(top, tolerance=tolerance) + vst = Face.InternalVertex(top, tolerance=tolerance, silent=silent) elif Topology.IsInstance(top, "Wire"): #Wire if top.IsClosed(): internalBoundaries = [] try: tempFace = topologic.Face.ByExternalInternalBoundaries(top, internalBoundaries) - vst = Face.InternalVertex(tempFace, tolerance=tolerance) + vst = Face.InternalVertex(tempFace, tolerance=tolerance, silent=silent) except: vst = Topology.Centroid(top) else: @@ -6295,7 +6297,7 @@ def _InternalVertex(topology, tolerance: float = 0.0001): elif Topology.IsInstance(top, "Vertex"): #Vertex vst = top elif Topology.IsInstance(topology, "Aperture"): #Aperture - vst = Face.InternalVertex(Aperture.Topology(top), tolerance=tolerance) + vst = Face.InternalVertex(Aperture.Topology(top), tolerance=tolerance, silent=silent) else: vst = Topology.Centroid(top) return vst @@ -6303,7 +6305,7 @@ def _InternalVertex(topology, tolerance: float = 0.0001): @staticmethod - def InternalVertex(topology, timeout: int = 10, tolerance: float = 0.0001): + def InternalVertex(topology, timeout: int = 30, tolerance: float = 0.0001, silent: bool = False): """ Returns a vertex guaranteed to be inside the input topology. @@ -6312,9 +6314,11 @@ def InternalVertex(topology, timeout: int = 10, tolerance: float = 0.0001): topology : topologic_core.Topology The input topology. timeout : int , optional - The amount of seconds to wait before timing out. The default is 10 seconds. + The amount of seconds to wait before timing out. The default is 30 seconds. tolerance : float , ptional The desired tolerance. The default is 0.0001. + silent : bool , optional + If set to True, no error and warning messages are printed. Otherwise, they are. The default is False. Returns ------- @@ -6325,22 +6329,21 @@ def InternalVertex(topology, timeout: int = 10, tolerance: float = 0.0001): import concurrent.futures import time # Wrapper function with timeout - def run_with_timeout(func, topology, tolerance=0.0001, timeout=10): + def run_with_timeout(func, topology, tolerance=0.0001, silent=False, timeout=10): with concurrent.futures.ThreadPoolExecutor() as executor: - future = executor.submit(func, topology, tolerance=tolerance) + future = executor.submit(func, topology, tolerance=tolerance, silent=silent) try: result = future.result(timeout=timeout) # Wait for the result with a timeout return result except concurrent.futures.TimeoutError: - print("Function took too long, retrying with a different solution.") return None # or try another approach here - result = run_with_timeout(Topology._InternalVertex, topology=topology, tolerance=tolerance, timeout=timeout) # Set a 10 second timeout + result = run_with_timeout(Topology._InternalVertex, topology=topology, tolerance=tolerance, silent=silent, timeout=timeout) # Set a 10 second timeout if result is None: # Handle failure case (e.g., try a different solution) - print("Using a different approach.") - result = Topology.Centroid(topology) - print("Result is:", result) + if not silent: + print("Topology.InternalVertex - Warning: Operation took too long. Returning None") + return None return result @staticmethod diff --git a/src/topologicpy/version.py b/src/topologicpy/version.py index 3c28512..1621cbd 100644 --- a/src/topologicpy/version.py +++ b/src/topologicpy/version.py @@ -1 +1 @@ -__version__ = '0.7.87' +__version__ = '0.7.88'