Module Cluster

Expand source code
# Copyright (C) 2024
# Wassim Jabi <wassim.jabi@gmail.com>
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along with
# this program. If not, see <https://www.gnu.org/licenses/>.

import topologic_core as topologic
import os
import warnings

try:
    import numpy as np
except:
    print("Cluster - Installing required numpy library.")
    try:
        os.system("pip install numpy")
    except:
        os.system("pip install numpy --user")
    try:
        import numpy as np
        print("Cluster - numpy library installed correctly.")
    except:
        warnings.warn("Cluster - Error: Could not import numpy.")

try:
    from scipy.spatial.distance import pdist, squareform
except:
    print("Cluster - Installing required scipy library.")
    try:
        os.system("pip install scipy")
    except:
        os.system("pip install scipy --user")
    try:
        from scipy.spatial.distance import pdist, squareform
        print("Cluster - scipy library installed correctly.")
    except:
        warnings.warn("Cluster - Error: Could not import scipy.")

class Cluster():
    @staticmethod
    def ByFormula(formula, xRange=None, yRange=None, xString="X", yString="Y"):
        """
        Creates a cluster of vertices by evaluating the input formula for a range of x values and, optionally, a range of y values.

        Parameters
        ----------
        formula : str
            A string representing the formula to be evaluated.
            For 2D formulas (i.e. Z = 0), use either 'X' (uppercase) or 'Y' (uppercase) for the independent variable.
            For 3D formulas, use 'X' and 'Y' (uppercase) for the independent variables. The Z value will be evaluated.
            For 3D formulas, both xRange and yRange MUST be specified.
            You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc.
            For example, 'X**2 + 2*X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
        xRange : tuple , optional
            A tuple (start, end, step) representing the range of X values for which the formula should be evaluated.
            For example, to evaluate Y for X values from -5 to 5 with a step of 0.1, you should specify xRange=(-5, 5, 0.1).
            If the xRange is set to None or not specified:
                The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y**2 + 2*Y - sqrt(Y)')
                The method will attempt to evaluate X based on the specified yRange.
                xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
        yRange : tuple , optional
            A tuple (start, end, step) representing the range of Y values for which the formula should be evaluated.
            For example, to evaluate X for Y values from -5 to 5 with a step of 0.1, you should specify yRange=(-5,5,0.1).
            If the yRange is set to None or not specified:
                The method assumes that the formula uses the xString (e.g. 'X' as in 'X**2 + 2*X - sqrt(X)')
                The method will attempt to evaluate Y based on the specified xRange.
                xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
        xString : str , optional
            The string used to represent the X independent variable. The default is 'X' (uppercase).
        yString : str , optional
            The string used to represent the Y independent variable. The default is 'Y' (uppercase).

        Returns:
            topologic_core.Cluster
                The created cluster of vertices.
        """
        from topologicpy.Vertex import Vertex
        import math
        if xRange == None and yRange == None:
            print("Cluster.ByFormula - Error: Both ranges cannot be None at the same time. Returning None.")
            return None
        if xString.islower():
            print("Cluster.ByFormula - Error: the input xString cannot lowercase. Please consider using uppercase (e.g. X). Returning None.")
            return None
        if yString == 'y':
            print("Cluster.ByFormula - Error: the input yString cannot be lowercase. Please consider using uppercase (e.g. Y). Returning None.")
            return None
        
        x_values = []
        y_values = []
        if not xRange == None:
            x_start, x_end, x_step = xRange
            x = x_start
            while x < x_end:
                x_values.append(x)
                x = x + x_step
            x_values.append(x_end)
        
        if not yRange == None:
            y_start, y_end, y_step = yRange
            y = y_start
            while y < y_end:
                y_values.append(y)
                y = y + y_step
            y_values.append(y_end)

        # Evaluate the formula for each x and y value
        x_return = []
        y_return = []
        z_return = []
        if len(x_values) > 0 and len(y_values) > 0: # Both X and Y exist, compute Z.
            for x in x_values:
                for y in y_values:
                    x_return.append(x)
                    y_return.append(y)
                    formula1 = formula.replace(xString, str(x)).replace(yString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                    z_return.append(eval(formula1))
        elif len(x_values) == 0 and len(y_values) > 0: # Only Y exists, compute X, Z is always 0.
            for y in y_values:
                y_return.append(y)
                formula1 = formula.replace(xString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                x_return.append(eval(formula1))
                z_return.append(0)
        else: # Only X exists, compute Y, Z is always 0.
            for x in x_values:
                x_return.append(x)
                formula1 = formula.replace(xString, str(x)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                y_return.append(eval(formula1))
                z_return.append(0)
        vertices = []
        for i in range(len(x_return)):
            vertices.append(Vertex.ByCoordinates(x_return[i], y_return[i], z_return[i]))
        return Cluster.ByTopologies(vertices)
    
    @staticmethod
    def ByTopologies(*args, transferDictionaries: bool = False):
        """
        Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.

        Parameters
        ----------
        topologies : list
            The list of topologies.
        transferDictionaries : bool , optional
            If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.

        Returns
        -------
        topologic_core.Cluster
            The created topologic Cluster.

        """
        from topologicpy.Dictionary import Dictionary
        from topologicpy.Topology import Topology
        from topologicpy.Helper import Helper

        if len(args) == 0:
            print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
            return None
        if len(args) == 1:
            topologies = args[0]
            if isinstance(topologies, list):
                if len(topologies) == 0:
                    print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
                    return None
                else:
                    topologyList = [x for x in topologies if Topology.IsInstance(x, "Topology")]
                    if len(topologies) == 0:
                        print("Cluster.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
                        return None
            else:
                print("Cluster.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
                return topologies
        else:
            topologyList = Helper.Flatten(list(args))
            topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
        if len(topologyList) == 0:
            print("Cluster.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
            return None
        cluster = topologic.Cluster.ByTopologies(topologyList, False) # Hook to Core
        dictionaries = []
        for t in topologyList:
            d = Topology.Dictionary(t)
            keys = Dictionary.Keys(d)
            if isinstance(keys, list):
                if len(keys) > 0:
                    dictionaries.append(d)
        if len(dictionaries) > 0:
            if len(dictionaries) > 1:
                d = Dictionary.ByMergedDictionaries(dictionaries)
            else:
                d = dictionaries[0]
                cluster = Topology.SetDictionary(cluster, d)
        return cluster

    @staticmethod
    def CellComplexes(cluster) -> list:
        """
        Returns the cellComplexes of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of cellComplexes.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.CellComplexes - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cellComplexes = []
        _ = cluster.CellComplexes(None, cellComplexes)
        return cellComplexes

    @staticmethod
    def Cells(cluster) -> list:
        """
        Returns the cells of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of cells.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Cells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cells = []
        _ = cluster.Cells(None, cells)
        return cells


    @staticmethod
    def DBSCAN(topologies, selectors=None, keys=["x", "y", "z"], epsilon: float = 0.5, minSamples: int = 2):
        """
        Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN

        Parameters
        ----------
        topologies : list
            The input list of topologies to be clustered.
        selectors : list , optional
            If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
            If set to None, the list of topologies is expected to be a list of vertices. The default is None.
        keys : list, optional
            The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
            make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
        epsilon : float , optional
            The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5. 
        minSamples : int , optional
            The minimum number of points required to form a dense region (cluster). The default is 2.

        Returns
        -------
        list, list
            The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).

        """
        from topologicpy.Vertex import Vertex
        from topologicpy.Topology import Topology
        from topologicpy.Dictionary import Dictionary

        def dbscan_3d_indices(data, eps, min_samples):
            """
            DBSCAN clustering algorithm for 3D points.

            Parameters:
            - data: NumPy array, input data points with X, Y, and Z coordinates.
            - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
            - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.

            Returns:
            - clusters: List of lists, each list containing the indices of points in a cluster.
            - noise: List of indices, indices of points labeled as noise.
            """

            # Compute pairwise distances
            dists = squareform(pdist(data))

            # Initialize labels and cluster ID
            labels = np.full(data.shape[0], -1)
            cluster_id = 0

            # Iterate through each point
            for i in range(data.shape[0]):
                if labels[i] != -1:
                    continue  # Skip already processed points

                # Find neighbors within epsilon distance
                neighbors = np.where(dists[i] < eps)[0]

                if len(neighbors) < min_samples:
                    # Label as noise
                    labels[i] = -1
                else:
                    # Expand cluster
                    cluster_id += 1
                    expand_cluster_3d_indices(labels, dists, i, neighbors, cluster_id, eps, min_samples)

            # Organize indices into clusters and noise
            clusters = [list(np.where(labels == cid)[0]) for cid in range(1, cluster_id + 1)]
            noise = list(np.where(labels == -1)[0])

            return clusters, noise

        def expand_cluster_3d_indices(labels, dists, point_index, neighbors, cluster_id, eps, min_samples):
            """
            Expand the cluster around a core point for 3D points.

            Parameters:
            - labels: NumPy array, cluster labels for each data point.
            - dists: NumPy array, pairwise distances between data points.
            - point_index: int, index of the core point.
            - neighbors: NumPy array, indices of neighbors.
            - cluster_id: int, current cluster ID.
            - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
            - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
            """
            labels[point_index] = cluster_id

            i = 0
            while i < len(neighbors):
                current_neighbor = neighbors[i]

                if labels[current_neighbor] == -1:
                    labels[current_neighbor] = cluster_id

                    new_neighbors = np.where(dists[current_neighbor] < eps)[0]
                    if len(new_neighbors) >= min_samples:
                        neighbors = np.concatenate([neighbors, new_neighbors])

                elif labels[current_neighbor] == 0:
                    labels[current_neighbor] = cluster_id

                i += 1
        
        if not isinstance(topologies, list):
            print("Cluster.DBSCAN - Error: The input vertices parameter is not a valid list. Returning None.")
            return None, None
        topologyList = [t for t in topologies if Topology.IsInstance(t, "Topology")]
        if len(topologyList) < 1:
            print("Cluster.DBSCAN - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
            return None, None
        if len(topologyList) < minSamples:
            print("Cluster.DBSCAN - Error: The input minSamples parameter cannot be larger than the number of vertices. Returning None.")
            return None, None
        
        if not isinstance(selectors, list):
            check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.DBSCAN - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None, None
        else:
            selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
            if len(selectors) < 1:
                check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
                if len(check_vertices) > 0:
                    print("Cluster.DBSCAN - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                    return None, None
            if not len(selectors) == len(topologyList):
                print("Cluster.DBSCAN - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
                return None, None
        if not isinstance(keys, list):
            print("Cluster.DBSCAN - Error: The input keys parameter is not a valid list. Returning None.")
            return None
        

        data = []
        if selectors == None:
            for t in topologyList:
                elements = []
                if keys:
                    d = Topology.Dictionary(t)
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        else:
            for i, s in enumerate(selectors):
                elements = []
                if keys:
                    d = Topology.Dictionary(topologyList[i])
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(s)
                        elif key.lower() == "y":
                            value = Vertex.Y(s)
                        elif key.lower() == "z":
                            value = Vertex.Z(s)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        #coords = [[Vertex.X(v), Vertex.Y(v), Vertex.Z(v)] for v in vertexList]
        clusters, noise = dbscan_3d_indices(np.array(data), epsilon, minSamples)
        tp_clusters = []
        for cluster in clusters:
            tp_clusters.append(Cluster.ByTopologies([topologyList[i] for i in cluster]))
        vert_group = []
        tp_noise = None
        if len(noise) > 0:
            tp_noise = Cluster.ByTopologies([topologyList[i] for i in noise])
        return tp_clusters, tp_noise

    @staticmethod
    def Edges(cluster) -> list:
        """
        Returns the edges of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of edges.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Edges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        edges = []
        _ = cluster.Edges(None, edges)
        return edges

    @staticmethod
    def Faces(cluster) -> list:
        """
        Returns the faces of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of faces.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Faces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        faces = []
        _ = cluster.Faces(None, faces)
        return faces

    @staticmethod
    def FreeCells(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free cells of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free cells.

        """
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeCells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allCells = []
        _ = cluster.Cells(None, allCells)
        if len(allCells) < 1:
            return []
        allCellsCluster = Cluster.ByTopologies(allCells)
        freeCells = []
        cellComplexes = []
        _ = cluster.CellComplexes(None, cellComplexes)
        cellComplexesCells = []
        for cellComplex in cellComplexes:
            tempCells = CellComplex.Cells(cellComplex)
            cellComplexesCells += tempCells
        if len(cellComplexesCells) == 0:
            return allCells
        cellComplexesCluster = Cluster.ByTopologies(cellComplexesCells)
        resultingCluster = Topology.Boolean(allCellsCluster, cellComplexesCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Cell"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="cell")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeShells(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free shells of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free shells.

        """
        from topologicpy.Cell import Cell
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeShells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allShells = []
        _ = cluster.Shells(None, allShells)
        if len(allShells) < 1:
            return []
        allShellsCluster = Cluster.ByTopologies(allShells)
        cells = []
        _ = cluster.Cells(None, cells)
        cellsShells = []
        for cell in cells:
            tempShells = Cell.Shells(cell)
            cellsShells += tempShells
        if len(cellsShells) == 0:
            return allShells
        cellsCluster = Cluster.ByTopologies(cellsShells)
        resultingCluster = Topology.Boolean(allShellsCluster, cellsCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Shell"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="shell")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeFaces(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free faces of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free faces.

        """
        from topologicpy.Shell import Shell
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeFaces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allFaces = []
        _ = cluster.Faces(None, allFaces)
        if len(allFaces) < 1:
            return []
        allFacesCluster = Cluster.ByTopologies(allFaces)
        shells = []
        _ = cluster.Shells(None, shells)
        shellFaces = []
        for shell in shells:
            tempFaces = Shell.Faces(shell)
            shellFaces += tempFaces
        if len(shellFaces) == 0:
            return allFaces
        shellCluster = Cluster.ByTopologies(shellFaces)
        resultingCluster = Topology.Boolean(allFacesCluster, shellCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Face"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="face")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result

    @staticmethod
    def FreeWires(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free wires of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free wires.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeWires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allWires = []
        _ = cluster.Wires(None, allWires)
        if len(allWires) < 1:
            return []
        allWiresCluster = Cluster.ByTopologies(allWires)
        faces = []
        _ = cluster.Faces(None, faces)
        facesWires = []
        for face in faces:
            tempWires = Face.Wires(face)
            facesWires += tempWires
        if len(facesWires) == 0:
            return allWires
        facesCluster = Cluster.ByTopologies(facesWires)
        resultingCluster = Topology.Boolean(allWiresCluster, facesCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Wire"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="wire")
        if not result:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeEdges(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free edges of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free edges.

        """
        from topologicpy.Wire import Wire
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allEdges = []
        _ = cluster.Edges(None, allEdges)
        if len(allEdges) < 1:
            return []
        allEdgesCluster = Cluster.ByTopologies(allEdges)
        wires = []
        _ = cluster.Wires(None, wires)
        wireEdges = []
        for wire in wires:
            tempEdges = Wire.Edges(wire)
            wireEdges += tempEdges
        if len(wireEdges) == 0:
            return allEdges
        wireCluster = Cluster.ByTopologies(wireEdges)
        resultingCluster = Topology.Boolean(allEdgesCluster, wireCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Edge"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="edge")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeVertices(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free vertices of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free vertices.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeVertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allVertices = []
        _ = cluster.Vertices(None, allVertices)
        if len(allVertices) < 1:
            return []
        allVerticesCluster = Cluster.ByTopologies(allVertices)
        edges = []
        _ = cluster.Edges(None, edges)
        edgesVertices = []
        for edge in edges:
            tempVertices = Edge.Vertices(edge)
            edgesVertices += tempVertices
        if len(edgesVertices) == 0:
            return allVertices
        edgesCluster = Cluster.ByTopologies(edgesVertices)
        resultingCluster = Topology.Boolean(allVerticesCluster, edgesCluster, operation="difference", tolerance=tolerance)
        if Topology.IsInstance(resultingCluster, "Vertex"):
            return [resultingCluster]
        if resultingCluster == None:
            return []
        result = Topology.SubTopologies(resultingCluster, subTopologyType="vertex")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeTopologies(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free topologies of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free topologies.

        """
        topologies = Cluster.FreeVertices(cluster, tolerance=tolerance)
        topologies += Cluster.FreeEdges(cluster, tolerance=tolerance)
        topologies += Cluster.FreeWires(cluster, tolerance=tolerance)
        topologies += Cluster.FreeFaces(cluster, tolerance=tolerance)
        topologies += Cluster.FreeShells(cluster, tolerance=tolerance)
        topologies += Cluster.FreeCells(cluster, tolerance=tolerance)
        topologies += Cluster.CellComplexes(cluster)

        return topologies
    
    @staticmethod
    def HighestType(cluster) -> int:
        """
        Returns the type of the highest dimension subtopology found in the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        int
            The type of the highest dimension subtopology found in the input cluster.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.HighestType - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cellComplexes = Cluster.CellComplexes(cluster)
        if len(cellComplexes) > 0:
            return Topology.TypeID("CellComplex")
        cells = Cluster.Cells(cluster)
        if len(cells) > 0:
            return Topology.TypeID("Cell")
        shells = Cluster.Shells(cluster)
        if len(shells) > 0:
            return Topology.TypeID("Shell")
        faces = Cluster.Faces(cluster)
        if len(faces) > 0:
            return Topology.TypeID("Face")
        wires = Cluster.Wires(cluster)
        if len(wires) > 0:
            return Topology.TypeID("Wire")
        edges = Cluster.Edges(cluster)
        if len(edges) > 0:
            return Topology.TypeID("Edge")
        vertices = Cluster.Vertices(cluster)
        if len(vertices) > 0:
            return Topology.TypeID("Vertex")
    
    @staticmethod
    def K_Means(topologies, selectors=None, keys=["x", "y", "z"], k=4, maxIterations=100, centroidKey="k_centroid"):
        """
        Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering

        Parameters
        ----------
        topologies : list
            The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
        selectors : list , optional
            If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
            If set to None, the list of topologies is expected to be a list of vertices. The default is None.
        keys : list, optional
            The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
            make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
        k : int , optional
            The desired number of clusters. The default is 4.
        maxIterations : int , optional
            The desired maximum number of iterations for the clustering algorithm
        centroidKey : str , optional
            The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"

        Returns
        -------
        list
            The created list of clusters.

        """
        from topologicpy.Helper import Helper
        from topologicpy.Vertex import Vertex
        from topologicpy.Dictionary import Dictionary
        from topologicpy.Topology import Topology


        def k_means(data, vertices, k=4, maxIterations=100):
            import random
            def euclidean_distance(p, q):
                return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5

            # Initialize k centroids randomly
            centroids = random.sample(data, k)

            for _ in range(maxIterations):
                # Assign each data point to the nearest centroid
                clusters = [[] for _ in range(k)]
                clusters_v = [[] for _ in range(k)]
                for i, point in enumerate(data):
                    distances = [euclidean_distance(point, centroid) for centroid in centroids]
                    nearest_centroid_index = distances.index(min(distances))
                    clusters[nearest_centroid_index].append(point)
                    clusters_v[nearest_centroid_index].append(vertices[i])

                # Compute the new centroids as the mean of the points in each cluster
                new_centroids = []
                for cluster in clusters:
                    if not cluster:
                        # If a cluster is empty, keep the previous centroid
                        new_centroids.append(centroids[clusters.index(cluster)])
                    else:
                        new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])

                # Check if the centroids have converged
                if new_centroids == centroids:
                    break

                centroids = new_centroids

            return {'clusters': clusters, 'clusters_v': clusters_v, 'centroids': centroids}



        if not isinstance(topologies, list):
            print("Cluster.K_Means - Error: The input topologies parameter is not a valid list. Returning None.")
            return None
        topologies = [t for t in topologies if Topology.IsInstance(t, "Topology")]
        if len(topologies) < 1:
            print("Cluster.K_Means - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
            return None
        if not isinstance(selectors, list):
            check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.K_Means - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None
        else:
            selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
            if len(selectors) < 1:
                check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
                if len(check_vertices) > 0:
                    print("Cluster.K_Means - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                    return None
            if not len(selectors) == len(topologies):
                print("Cluster.K_Means - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
                return None
        if not isinstance(keys, list):
            print("Cluster.K_Means - Error: The input keys parameter is not a valid list. Returning None.")
            return None
        if not isinstance(k , int):
            print("Cluster.K_Means - Error: The input k parameter is not a valid integer. Returning None.")
            return None
        if k < 1:
            print("Cluster.K_Means - Error: The input k parameter is less than one. Returning None.")
            return None
        if len(topologies) < k:
            print("Cluster.K_Means - Error: The input topologies parameter is less than the specified number of clusters. Returning None.")
            return None
        if len(topologies) == k:
            t_clusters = []
            for topology in topologies:
                t_cluster = Cluster.ByTopologies([topology])
                for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                d = Dictionary.ByKeysValues([centroidKey], [elements])
                t_cluster = Topology.SetDictionary(t_cluster, d)
                t_clusters.append(t_cluster)
            return t_clusters
        
        data = []
        if selectors == None:
            for t in topologies:
                elements = []
                if keys:
                    d = Topology.Dictionary(t)
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        else:
            for i, s in enumerate(selectors):
                elements = []
                if keys:
                    d = Topology.Dictionary(topologies[i])
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(s)
                        elif key.lower() == "y":
                            value = Vertex.Y(s)
                        elif key.lower() == "z":
                            value = Vertex.Z(s)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        if len(data) == 0:
            print("Cluster.K_Means - Error: Could not perform the operation. Returning None.")
            return None
        if selectors:
            dict = k_means(data, selectors, k=k, maxIterations=maxIterations)
        else:
            dict = k_means(data, topologies, k=k, maxIterations=maxIterations)
        clusters = dict['clusters_v']
        centroids = dict['centroids']
        t_clusters = []
        for i, cluster in enumerate(clusters):
            cluster_vertices = []
            for v in cluster:
                if selectors == None:
                    cluster_vertices.append(v)
                else:
                    index = selectors.index(v)
                    cluster_vertices.append(topologies[index])
            cluster = Cluster.ByTopologies(cluster_vertices)
            d = Dictionary.ByKeysValues([centroidKey], [centroids[i]])
            cluster = Topology.SetDictionary(cluster, d)
            t_clusters.append(cluster)
        return t_clusters

    @staticmethod
    def MergeCells(cells, tolerance=0.0001):
        """
        Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.

        Parameters
        ----------
        cells : list
            The input list of cells.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Cluster
            The created cluster with merged cells as possible.

        """

        from topologicpy.CellComplex import CellComplex
        from topologicpy.Topology import Topology

        def find_cell_complexes(cells, adjacency_test, tolerance=0.0001):
            cell_complexes = []
            remaining_cells = set(cells)

            def explore_complex(cell_complex, remaining, tolerance=0.0001):
                new_cells = set()
                for cell in remaining:
                    if any(adjacency_test(cell, existing_cell, tolerance=tolerance) for existing_cell in cell_complex):
                        new_cells.add(cell)
                return new_cells

            while remaining_cells:
                current_cell = remaining_cells.pop()
                current_complex = {current_cell}
                current_complex.update(explore_complex(current_complex, remaining_cells, tolerance=tolerance))
                cell_complexes.append(current_complex)
                remaining_cells -= current_complex

            return cell_complexes

        # Example adjacency test function (replace this with your actual implementation)
        def adjacency_test(cell1, cell2, tolerance=0.0001):
            return Topology.IsInstance(Topology.Merge(cell1, cell2, tolerance=tolerance), "CellComplex")

        if not isinstance(cells, list):
            print("Cluster.MergeCells - Error: The input cells parameter is not a valid list of cells. Returning None.")
            return None
        #cells = [cell for cell in cells if Topology.IsInstance(cell, "Cell")]
        if len(cells) < 1:
            print("Cluster.MergeCells - Error: The input cells parameter does not contain any valid cells. Returning None.")
            return None
        
        complexes = find_cell_complexes(cells, adjacency_test)
        cellComplexes = []
        cells = []
        for aComplex in complexes:
            aComplex = list(aComplex)
            if len(aComplex) > 1:
                cc = CellComplex.ByCells(aComplex, silent=True)
                if Topology.IsInstance(cc, "CellComplex"):
                    cellComplexes.append(cc)
            elif len(aComplex) == 1:
                if Topology.IsInstance(aComplex[0], "Cell"):
                    cells.append(aComplex[0])
        return Cluster.ByTopologies(cellComplexes+cells)
    
    @staticmethod
    def MysticRose(wire= None, origin= None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement:str = "center", tolerance: float = 0.0001):
        """
        Creates a mystic rose.

        Parameters
        ----------
        wire : topologic_core.Wire , optional
            The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
        origin : topologic_core.Vertex , optional
            The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
        radius : float , optional
            The radius of the mystic rose. The default is 1.
        sides : int , optional
            The number of sides of the mystic rose. The default is 16.
        perimeter : bool , optional
            If True, the perimeter edges are included in the output. The default is True.
        direction : list , optional
            The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
        placement : str , optional
            The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Cluster
            The created mystic rose (cluster of edges).

        """
        import topologicpy
        from topologicpy.Vertex import Vertex
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Cluster import Cluster
        from itertools import combinations

        if wire == None:
            wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=0, toAngle=360, close=True, direction=direction, placement=placement, tolerance=tolerance)
        if not Wire.IsClosed(wire):
            print("Cluster.MysticRose - Error: The input wire parameter is not a closed topologic wire. Returning None.")
            return None
        vertices = Wire.Vertices(wire)
        indices = list(range(len(vertices)))
        combs = [[comb[0],comb[1]] for comb in combinations(indices, 2) if not (abs(comb[0]-comb[1]) == 1) and not (abs(comb[0]-comb[1]) == len(indices)-1)]
        edges = []
        if perimeter:
            edges = Wire.Edges(wire)
        for comb in combs:
            edges.append(Edge.ByVertices([vertices[comb[0]], vertices[comb[1]]], tolerance=tolerance))
        return Cluster.ByTopologies(edges)
    
    @staticmethod
    def Shells(cluster) -> list:
        """
        Returns the shells of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of shells.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Shells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        shells = []
        _ = cluster.Shells(None, shells)
        return shells

    @staticmethod
    def Simplify(cluster):
        """
        Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        topologic_core.Topology or list
            The simplification of the cluster.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Simplify - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        resultingTopologies = []
        topCC = []
        _ = cluster.CellComplexes(None, topCC)
        topCells = []
        _ = cluster.Cells(None, topCells)
        topShells = []
        _ = cluster.Shells(None, topShells)
        topFaces = []
        _ = cluster.Faces(None, topFaces)
        topWires = []
        _ = cluster.Wires(None, topWires)
        topEdges = []
        _ = cluster.Edges(None, topEdges)
        topVertices = []
        _ = cluster.Vertices(None, topVertices)
        if len(topCC) == 1:
            cc = topCC[0]
            ccVertices = []
            _ = cc.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(cc)
        if len(topCC) == 0 and len(topCells) == 1:
            cell = topCells[0]
            ccVertices = []
            _ = cell.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(cell)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 1:
            shell = topShells[0]
            ccVertices = []
            _ = shell.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(shell)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 1:
            face = topFaces[0]
            ccVertices = []
            _ = face.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(face)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 1:
            wire = topWires[0]
            ccVertices = []
            _ = wire.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(wire)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 1:
            edge = topEdges[0]
            ccVertices = []
            _ = wire.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(edge)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 0 and len(topVertices) == 1:
            vertex = topVertices[0]
            resultingTopologies.append(vertex)
        if len(resultingTopologies) == 1:
            return resultingTopologies[0]
        return cluster

    @staticmethod
    def Vertices(cluster) -> list:
        """
        Returns the vertices of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of vertices.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Vertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        vertices = []
        _ = cluster.Vertices(None, vertices)
        return vertices

    @staticmethod
    def Wires(cluster) -> list:
        """
        Returns the wires of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of wires.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Wires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        wires = []
        _ = cluster.Wires(None, wires)
        return wires

    

Classes

class Cluster
Expand source code
class Cluster():
    @staticmethod
    def ByFormula(formula, xRange=None, yRange=None, xString="X", yString="Y"):
        """
        Creates a cluster of vertices by evaluating the input formula for a range of x values and, optionally, a range of y values.

        Parameters
        ----------
        formula : str
            A string representing the formula to be evaluated.
            For 2D formulas (i.e. Z = 0), use either 'X' (uppercase) or 'Y' (uppercase) for the independent variable.
            For 3D formulas, use 'X' and 'Y' (uppercase) for the independent variables. The Z value will be evaluated.
            For 3D formulas, both xRange and yRange MUST be specified.
            You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc.
            For example, 'X**2 + 2*X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
        xRange : tuple , optional
            A tuple (start, end, step) representing the range of X values for which the formula should be evaluated.
            For example, to evaluate Y for X values from -5 to 5 with a step of 0.1, you should specify xRange=(-5, 5, 0.1).
            If the xRange is set to None or not specified:
                The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y**2 + 2*Y - sqrt(Y)')
                The method will attempt to evaluate X based on the specified yRange.
                xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
        yRange : tuple , optional
            A tuple (start, end, step) representing the range of Y values for which the formula should be evaluated.
            For example, to evaluate X for Y values from -5 to 5 with a step of 0.1, you should specify yRange=(-5,5,0.1).
            If the yRange is set to None or not specified:
                The method assumes that the formula uses the xString (e.g. 'X' as in 'X**2 + 2*X - sqrt(X)')
                The method will attempt to evaluate Y based on the specified xRange.
                xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
        xString : str , optional
            The string used to represent the X independent variable. The default is 'X' (uppercase).
        yString : str , optional
            The string used to represent the Y independent variable. The default is 'Y' (uppercase).

        Returns:
            topologic_core.Cluster
                The created cluster of vertices.
        """
        from topologicpy.Vertex import Vertex
        import math
        if xRange == None and yRange == None:
            print("Cluster.ByFormula - Error: Both ranges cannot be None at the same time. Returning None.")
            return None
        if xString.islower():
            print("Cluster.ByFormula - Error: the input xString cannot lowercase. Please consider using uppercase (e.g. X). Returning None.")
            return None
        if yString == 'y':
            print("Cluster.ByFormula - Error: the input yString cannot be lowercase. Please consider using uppercase (e.g. Y). Returning None.")
            return None
        
        x_values = []
        y_values = []
        if not xRange == None:
            x_start, x_end, x_step = xRange
            x = x_start
            while x < x_end:
                x_values.append(x)
                x = x + x_step
            x_values.append(x_end)
        
        if not yRange == None:
            y_start, y_end, y_step = yRange
            y = y_start
            while y < y_end:
                y_values.append(y)
                y = y + y_step
            y_values.append(y_end)

        # Evaluate the formula for each x and y value
        x_return = []
        y_return = []
        z_return = []
        if len(x_values) > 0 and len(y_values) > 0: # Both X and Y exist, compute Z.
            for x in x_values:
                for y in y_values:
                    x_return.append(x)
                    y_return.append(y)
                    formula1 = formula.replace(xString, str(x)).replace(yString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                    z_return.append(eval(formula1))
        elif len(x_values) == 0 and len(y_values) > 0: # Only Y exists, compute X, Z is always 0.
            for y in y_values:
                y_return.append(y)
                formula1 = formula.replace(xString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                x_return.append(eval(formula1))
                z_return.append(0)
        else: # Only X exists, compute Y, Z is always 0.
            for x in x_values:
                x_return.append(x)
                formula1 = formula.replace(xString, str(x)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                y_return.append(eval(formula1))
                z_return.append(0)
        vertices = []
        for i in range(len(x_return)):
            vertices.append(Vertex.ByCoordinates(x_return[i], y_return[i], z_return[i]))
        return Cluster.ByTopologies(vertices)
    
    @staticmethod
    def ByTopologies(*args, transferDictionaries: bool = False):
        """
        Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.

        Parameters
        ----------
        topologies : list
            The list of topologies.
        transferDictionaries : bool , optional
            If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.

        Returns
        -------
        topologic_core.Cluster
            The created topologic Cluster.

        """
        from topologicpy.Dictionary import Dictionary
        from topologicpy.Topology import Topology
        from topologicpy.Helper import Helper

        if len(args) == 0:
            print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
            return None
        if len(args) == 1:
            topologies = args[0]
            if isinstance(topologies, list):
                if len(topologies) == 0:
                    print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
                    return None
                else:
                    topologyList = [x for x in topologies if Topology.IsInstance(x, "Topology")]
                    if len(topologies) == 0:
                        print("Cluster.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
                        return None
            else:
                print("Cluster.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
                return topologies
        else:
            topologyList = Helper.Flatten(list(args))
            topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
        if len(topologyList) == 0:
            print("Cluster.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
            return None
        cluster = topologic.Cluster.ByTopologies(topologyList, False) # Hook to Core
        dictionaries = []
        for t in topologyList:
            d = Topology.Dictionary(t)
            keys = Dictionary.Keys(d)
            if isinstance(keys, list):
                if len(keys) > 0:
                    dictionaries.append(d)
        if len(dictionaries) > 0:
            if len(dictionaries) > 1:
                d = Dictionary.ByMergedDictionaries(dictionaries)
            else:
                d = dictionaries[0]
                cluster = Topology.SetDictionary(cluster, d)
        return cluster

    @staticmethod
    def CellComplexes(cluster) -> list:
        """
        Returns the cellComplexes of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of cellComplexes.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.CellComplexes - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cellComplexes = []
        _ = cluster.CellComplexes(None, cellComplexes)
        return cellComplexes

    @staticmethod
    def Cells(cluster) -> list:
        """
        Returns the cells of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of cells.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Cells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cells = []
        _ = cluster.Cells(None, cells)
        return cells


    @staticmethod
    def DBSCAN(topologies, selectors=None, keys=["x", "y", "z"], epsilon: float = 0.5, minSamples: int = 2):
        """
        Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN

        Parameters
        ----------
        topologies : list
            The input list of topologies to be clustered.
        selectors : list , optional
            If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
            If set to None, the list of topologies is expected to be a list of vertices. The default is None.
        keys : list, optional
            The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
            make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
        epsilon : float , optional
            The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5. 
        minSamples : int , optional
            The minimum number of points required to form a dense region (cluster). The default is 2.

        Returns
        -------
        list, list
            The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).

        """
        from topologicpy.Vertex import Vertex
        from topologicpy.Topology import Topology
        from topologicpy.Dictionary import Dictionary

        def dbscan_3d_indices(data, eps, min_samples):
            """
            DBSCAN clustering algorithm for 3D points.

            Parameters:
            - data: NumPy array, input data points with X, Y, and Z coordinates.
            - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
            - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.

            Returns:
            - clusters: List of lists, each list containing the indices of points in a cluster.
            - noise: List of indices, indices of points labeled as noise.
            """

            # Compute pairwise distances
            dists = squareform(pdist(data))

            # Initialize labels and cluster ID
            labels = np.full(data.shape[0], -1)
            cluster_id = 0

            # Iterate through each point
            for i in range(data.shape[0]):
                if labels[i] != -1:
                    continue  # Skip already processed points

                # Find neighbors within epsilon distance
                neighbors = np.where(dists[i] < eps)[0]

                if len(neighbors) < min_samples:
                    # Label as noise
                    labels[i] = -1
                else:
                    # Expand cluster
                    cluster_id += 1
                    expand_cluster_3d_indices(labels, dists, i, neighbors, cluster_id, eps, min_samples)

            # Organize indices into clusters and noise
            clusters = [list(np.where(labels == cid)[0]) for cid in range(1, cluster_id + 1)]
            noise = list(np.where(labels == -1)[0])

            return clusters, noise

        def expand_cluster_3d_indices(labels, dists, point_index, neighbors, cluster_id, eps, min_samples):
            """
            Expand the cluster around a core point for 3D points.

            Parameters:
            - labels: NumPy array, cluster labels for each data point.
            - dists: NumPy array, pairwise distances between data points.
            - point_index: int, index of the core point.
            - neighbors: NumPy array, indices of neighbors.
            - cluster_id: int, current cluster ID.
            - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
            - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
            """
            labels[point_index] = cluster_id

            i = 0
            while i < len(neighbors):
                current_neighbor = neighbors[i]

                if labels[current_neighbor] == -1:
                    labels[current_neighbor] = cluster_id

                    new_neighbors = np.where(dists[current_neighbor] < eps)[0]
                    if len(new_neighbors) >= min_samples:
                        neighbors = np.concatenate([neighbors, new_neighbors])

                elif labels[current_neighbor] == 0:
                    labels[current_neighbor] = cluster_id

                i += 1
        
        if not isinstance(topologies, list):
            print("Cluster.DBSCAN - Error: The input vertices parameter is not a valid list. Returning None.")
            return None, None
        topologyList = [t for t in topologies if Topology.IsInstance(t, "Topology")]
        if len(topologyList) < 1:
            print("Cluster.DBSCAN - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
            return None, None
        if len(topologyList) < minSamples:
            print("Cluster.DBSCAN - Error: The input minSamples parameter cannot be larger than the number of vertices. Returning None.")
            return None, None
        
        if not isinstance(selectors, list):
            check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.DBSCAN - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None, None
        else:
            selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
            if len(selectors) < 1:
                check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
                if len(check_vertices) > 0:
                    print("Cluster.DBSCAN - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                    return None, None
            if not len(selectors) == len(topologyList):
                print("Cluster.DBSCAN - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
                return None, None
        if not isinstance(keys, list):
            print("Cluster.DBSCAN - Error: The input keys parameter is not a valid list. Returning None.")
            return None
        

        data = []
        if selectors == None:
            for t in topologyList:
                elements = []
                if keys:
                    d = Topology.Dictionary(t)
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        else:
            for i, s in enumerate(selectors):
                elements = []
                if keys:
                    d = Topology.Dictionary(topologyList[i])
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(s)
                        elif key.lower() == "y":
                            value = Vertex.Y(s)
                        elif key.lower() == "z":
                            value = Vertex.Z(s)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        #coords = [[Vertex.X(v), Vertex.Y(v), Vertex.Z(v)] for v in vertexList]
        clusters, noise = dbscan_3d_indices(np.array(data), epsilon, minSamples)
        tp_clusters = []
        for cluster in clusters:
            tp_clusters.append(Cluster.ByTopologies([topologyList[i] for i in cluster]))
        vert_group = []
        tp_noise = None
        if len(noise) > 0:
            tp_noise = Cluster.ByTopologies([topologyList[i] for i in noise])
        return tp_clusters, tp_noise

    @staticmethod
    def Edges(cluster) -> list:
        """
        Returns the edges of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of edges.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Edges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        edges = []
        _ = cluster.Edges(None, edges)
        return edges

    @staticmethod
    def Faces(cluster) -> list:
        """
        Returns the faces of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of faces.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Faces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        faces = []
        _ = cluster.Faces(None, faces)
        return faces

    @staticmethod
    def FreeCells(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free cells of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free cells.

        """
        from topologicpy.CellComplex import CellComplex
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeCells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allCells = []
        _ = cluster.Cells(None, allCells)
        if len(allCells) < 1:
            return []
        allCellsCluster = Cluster.ByTopologies(allCells)
        freeCells = []
        cellComplexes = []
        _ = cluster.CellComplexes(None, cellComplexes)
        cellComplexesCells = []
        for cellComplex in cellComplexes:
            tempCells = CellComplex.Cells(cellComplex)
            cellComplexesCells += tempCells
        if len(cellComplexesCells) == 0:
            return allCells
        cellComplexesCluster = Cluster.ByTopologies(cellComplexesCells)
        resultingCluster = Topology.Boolean(allCellsCluster, cellComplexesCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Cell"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="cell")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeShells(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free shells of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free shells.

        """
        from topologicpy.Cell import Cell
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeShells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allShells = []
        _ = cluster.Shells(None, allShells)
        if len(allShells) < 1:
            return []
        allShellsCluster = Cluster.ByTopologies(allShells)
        cells = []
        _ = cluster.Cells(None, cells)
        cellsShells = []
        for cell in cells:
            tempShells = Cell.Shells(cell)
            cellsShells += tempShells
        if len(cellsShells) == 0:
            return allShells
        cellsCluster = Cluster.ByTopologies(cellsShells)
        resultingCluster = Topology.Boolean(allShellsCluster, cellsCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Shell"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="shell")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeFaces(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free faces of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free faces.

        """
        from topologicpy.Shell import Shell
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeFaces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allFaces = []
        _ = cluster.Faces(None, allFaces)
        if len(allFaces) < 1:
            return []
        allFacesCluster = Cluster.ByTopologies(allFaces)
        shells = []
        _ = cluster.Shells(None, shells)
        shellFaces = []
        for shell in shells:
            tempFaces = Shell.Faces(shell)
            shellFaces += tempFaces
        if len(shellFaces) == 0:
            return allFaces
        shellCluster = Cluster.ByTopologies(shellFaces)
        resultingCluster = Topology.Boolean(allFacesCluster, shellCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Face"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="face")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result

    @staticmethod
    def FreeWires(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free wires of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free wires.

        """
        from topologicpy.Face import Face
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeWires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allWires = []
        _ = cluster.Wires(None, allWires)
        if len(allWires) < 1:
            return []
        allWiresCluster = Cluster.ByTopologies(allWires)
        faces = []
        _ = cluster.Faces(None, faces)
        facesWires = []
        for face in faces:
            tempWires = Face.Wires(face)
            facesWires += tempWires
        if len(facesWires) == 0:
            return allWires
        facesCluster = Cluster.ByTopologies(facesWires)
        resultingCluster = Topology.Boolean(allWiresCluster, facesCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Wire"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="wire")
        if not result:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeEdges(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free edges of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float, optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free edges.

        """
        from topologicpy.Wire import Wire
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allEdges = []
        _ = cluster.Edges(None, allEdges)
        if len(allEdges) < 1:
            return []
        allEdgesCluster = Cluster.ByTopologies(allEdges)
        wires = []
        _ = cluster.Wires(None, wires)
        wireEdges = []
        for wire in wires:
            tempEdges = Wire.Edges(wire)
            wireEdges += tempEdges
        if len(wireEdges) == 0:
            return allEdges
        wireCluster = Cluster.ByTopologies(wireEdges)
        resultingCluster = Topology.Boolean(allEdgesCluster, wireCluster, operation="difference", tolerance=tolerance)
        if resultingCluster == None:
            return []
        if Topology.IsInstance(resultingCluster, "Edge"):
            return [resultingCluster]
        result = Topology.SubTopologies(resultingCluster, subTopologyType="edge")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeVertices(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free vertices of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free vertices.

        """
        from topologicpy.Edge import Edge
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.FreeVertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        allVertices = []
        _ = cluster.Vertices(None, allVertices)
        if len(allVertices) < 1:
            return []
        allVerticesCluster = Cluster.ByTopologies(allVertices)
        edges = []
        _ = cluster.Edges(None, edges)
        edgesVertices = []
        for edge in edges:
            tempVertices = Edge.Vertices(edge)
            edgesVertices += tempVertices
        if len(edgesVertices) == 0:
            return allVertices
        edgesCluster = Cluster.ByTopologies(edgesVertices)
        resultingCluster = Topology.Boolean(allVerticesCluster, edgesCluster, operation="difference", tolerance=tolerance)
        if Topology.IsInstance(resultingCluster, "Vertex"):
            return [resultingCluster]
        if resultingCluster == None:
            return []
        result = Topology.SubTopologies(resultingCluster, subTopologyType="vertex")
        if result == None:
            return [] #Make sure you return an empty list instead of None
        return result
    
    @staticmethod
    def FreeTopologies(cluster, tolerance: float = 0.0001) -> list:
        """
        Returns the free topologies of the input cluster that are not part of a higher topology.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        list
            The list of free topologies.

        """
        topologies = Cluster.FreeVertices(cluster, tolerance=tolerance)
        topologies += Cluster.FreeEdges(cluster, tolerance=tolerance)
        topologies += Cluster.FreeWires(cluster, tolerance=tolerance)
        topologies += Cluster.FreeFaces(cluster, tolerance=tolerance)
        topologies += Cluster.FreeShells(cluster, tolerance=tolerance)
        topologies += Cluster.FreeCells(cluster, tolerance=tolerance)
        topologies += Cluster.CellComplexes(cluster)

        return topologies
    
    @staticmethod
    def HighestType(cluster) -> int:
        """
        Returns the type of the highest dimension subtopology found in the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        int
            The type of the highest dimension subtopology found in the input cluster.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.HighestType - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        cellComplexes = Cluster.CellComplexes(cluster)
        if len(cellComplexes) > 0:
            return Topology.TypeID("CellComplex")
        cells = Cluster.Cells(cluster)
        if len(cells) > 0:
            return Topology.TypeID("Cell")
        shells = Cluster.Shells(cluster)
        if len(shells) > 0:
            return Topology.TypeID("Shell")
        faces = Cluster.Faces(cluster)
        if len(faces) > 0:
            return Topology.TypeID("Face")
        wires = Cluster.Wires(cluster)
        if len(wires) > 0:
            return Topology.TypeID("Wire")
        edges = Cluster.Edges(cluster)
        if len(edges) > 0:
            return Topology.TypeID("Edge")
        vertices = Cluster.Vertices(cluster)
        if len(vertices) > 0:
            return Topology.TypeID("Vertex")
    
    @staticmethod
    def K_Means(topologies, selectors=None, keys=["x", "y", "z"], k=4, maxIterations=100, centroidKey="k_centroid"):
        """
        Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering

        Parameters
        ----------
        topologies : list
            The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
        selectors : list , optional
            If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
            If set to None, the list of topologies is expected to be a list of vertices. The default is None.
        keys : list, optional
            The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
            make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
        k : int , optional
            The desired number of clusters. The default is 4.
        maxIterations : int , optional
            The desired maximum number of iterations for the clustering algorithm
        centroidKey : str , optional
            The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"

        Returns
        -------
        list
            The created list of clusters.

        """
        from topologicpy.Helper import Helper
        from topologicpy.Vertex import Vertex
        from topologicpy.Dictionary import Dictionary
        from topologicpy.Topology import Topology


        def k_means(data, vertices, k=4, maxIterations=100):
            import random
            def euclidean_distance(p, q):
                return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5

            # Initialize k centroids randomly
            centroids = random.sample(data, k)

            for _ in range(maxIterations):
                # Assign each data point to the nearest centroid
                clusters = [[] for _ in range(k)]
                clusters_v = [[] for _ in range(k)]
                for i, point in enumerate(data):
                    distances = [euclidean_distance(point, centroid) for centroid in centroids]
                    nearest_centroid_index = distances.index(min(distances))
                    clusters[nearest_centroid_index].append(point)
                    clusters_v[nearest_centroid_index].append(vertices[i])

                # Compute the new centroids as the mean of the points in each cluster
                new_centroids = []
                for cluster in clusters:
                    if not cluster:
                        # If a cluster is empty, keep the previous centroid
                        new_centroids.append(centroids[clusters.index(cluster)])
                    else:
                        new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])

                # Check if the centroids have converged
                if new_centroids == centroids:
                    break

                centroids = new_centroids

            return {'clusters': clusters, 'clusters_v': clusters_v, 'centroids': centroids}



        if not isinstance(topologies, list):
            print("Cluster.K_Means - Error: The input topologies parameter is not a valid list. Returning None.")
            return None
        topologies = [t for t in topologies if Topology.IsInstance(t, "Topology")]
        if len(topologies) < 1:
            print("Cluster.K_Means - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
            return None
        if not isinstance(selectors, list):
            check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.K_Means - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None
        else:
            selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
            if len(selectors) < 1:
                check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
                if len(check_vertices) > 0:
                    print("Cluster.K_Means - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                    return None
            if not len(selectors) == len(topologies):
                print("Cluster.K_Means - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
                return None
        if not isinstance(keys, list):
            print("Cluster.K_Means - Error: The input keys parameter is not a valid list. Returning None.")
            return None
        if not isinstance(k , int):
            print("Cluster.K_Means - Error: The input k parameter is not a valid integer. Returning None.")
            return None
        if k < 1:
            print("Cluster.K_Means - Error: The input k parameter is less than one. Returning None.")
            return None
        if len(topologies) < k:
            print("Cluster.K_Means - Error: The input topologies parameter is less than the specified number of clusters. Returning None.")
            return None
        if len(topologies) == k:
            t_clusters = []
            for topology in topologies:
                t_cluster = Cluster.ByTopologies([topology])
                for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                d = Dictionary.ByKeysValues([centroidKey], [elements])
                t_cluster = Topology.SetDictionary(t_cluster, d)
                t_clusters.append(t_cluster)
            return t_clusters
        
        data = []
        if selectors == None:
            for t in topologies:
                elements = []
                if keys:
                    d = Topology.Dictionary(t)
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(t)
                        elif key.lower() == "y":
                            value = Vertex.Y(t)
                        elif key.lower() == "z":
                            value = Vertex.Z(t)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        else:
            for i, s in enumerate(selectors):
                elements = []
                if keys:
                    d = Topology.Dictionary(topologies[i])
                    for key in keys:
                        if key.lower() == "x":
                            value = Vertex.X(s)
                        elif key.lower() == "y":
                            value = Vertex.Y(s)
                        elif key.lower() == "z":
                            value = Vertex.Z(s)
                        else:
                            value = Dictionary.ValueAtKey(d, key)
                        if value != None:
                            elements.append(value)
                data.append(elements)
        if len(data) == 0:
            print("Cluster.K_Means - Error: Could not perform the operation. Returning None.")
            return None
        if selectors:
            dict = k_means(data, selectors, k=k, maxIterations=maxIterations)
        else:
            dict = k_means(data, topologies, k=k, maxIterations=maxIterations)
        clusters = dict['clusters_v']
        centroids = dict['centroids']
        t_clusters = []
        for i, cluster in enumerate(clusters):
            cluster_vertices = []
            for v in cluster:
                if selectors == None:
                    cluster_vertices.append(v)
                else:
                    index = selectors.index(v)
                    cluster_vertices.append(topologies[index])
            cluster = Cluster.ByTopologies(cluster_vertices)
            d = Dictionary.ByKeysValues([centroidKey], [centroids[i]])
            cluster = Topology.SetDictionary(cluster, d)
            t_clusters.append(cluster)
        return t_clusters

    @staticmethod
    def MergeCells(cells, tolerance=0.0001):
        """
        Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.

        Parameters
        ----------
        cells : list
            The input list of cells.
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Cluster
            The created cluster with merged cells as possible.

        """

        from topologicpy.CellComplex import CellComplex
        from topologicpy.Topology import Topology

        def find_cell_complexes(cells, adjacency_test, tolerance=0.0001):
            cell_complexes = []
            remaining_cells = set(cells)

            def explore_complex(cell_complex, remaining, tolerance=0.0001):
                new_cells = set()
                for cell in remaining:
                    if any(adjacency_test(cell, existing_cell, tolerance=tolerance) for existing_cell in cell_complex):
                        new_cells.add(cell)
                return new_cells

            while remaining_cells:
                current_cell = remaining_cells.pop()
                current_complex = {current_cell}
                current_complex.update(explore_complex(current_complex, remaining_cells, tolerance=tolerance))
                cell_complexes.append(current_complex)
                remaining_cells -= current_complex

            return cell_complexes

        # Example adjacency test function (replace this with your actual implementation)
        def adjacency_test(cell1, cell2, tolerance=0.0001):
            return Topology.IsInstance(Topology.Merge(cell1, cell2, tolerance=tolerance), "CellComplex")

        if not isinstance(cells, list):
            print("Cluster.MergeCells - Error: The input cells parameter is not a valid list of cells. Returning None.")
            return None
        #cells = [cell for cell in cells if Topology.IsInstance(cell, "Cell")]
        if len(cells) < 1:
            print("Cluster.MergeCells - Error: The input cells parameter does not contain any valid cells. Returning None.")
            return None
        
        complexes = find_cell_complexes(cells, adjacency_test)
        cellComplexes = []
        cells = []
        for aComplex in complexes:
            aComplex = list(aComplex)
            if len(aComplex) > 1:
                cc = CellComplex.ByCells(aComplex, silent=True)
                if Topology.IsInstance(cc, "CellComplex"):
                    cellComplexes.append(cc)
            elif len(aComplex) == 1:
                if Topology.IsInstance(aComplex[0], "Cell"):
                    cells.append(aComplex[0])
        return Cluster.ByTopologies(cellComplexes+cells)
    
    @staticmethod
    def MysticRose(wire= None, origin= None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement:str = "center", tolerance: float = 0.0001):
        """
        Creates a mystic rose.

        Parameters
        ----------
        wire : topologic_core.Wire , optional
            The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
        origin : topologic_core.Vertex , optional
            The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
        radius : float , optional
            The radius of the mystic rose. The default is 1.
        sides : int , optional
            The number of sides of the mystic rose. The default is 16.
        perimeter : bool , optional
            If True, the perimeter edges are included in the output. The default is True.
        direction : list , optional
            The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
        placement : str , optional
            The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
        tolerance : float , optional
            The desired tolerance. The default is 0.0001.

        Returns
        -------
        topologic_core.Cluster
            The created mystic rose (cluster of edges).

        """
        import topologicpy
        from topologicpy.Vertex import Vertex
        from topologicpy.Edge import Edge
        from topologicpy.Wire import Wire
        from topologicpy.Cluster import Cluster
        from itertools import combinations

        if wire == None:
            wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=0, toAngle=360, close=True, direction=direction, placement=placement, tolerance=tolerance)
        if not Wire.IsClosed(wire):
            print("Cluster.MysticRose - Error: The input wire parameter is not a closed topologic wire. Returning None.")
            return None
        vertices = Wire.Vertices(wire)
        indices = list(range(len(vertices)))
        combs = [[comb[0],comb[1]] for comb in combinations(indices, 2) if not (abs(comb[0]-comb[1]) == 1) and not (abs(comb[0]-comb[1]) == len(indices)-1)]
        edges = []
        if perimeter:
            edges = Wire.Edges(wire)
        for comb in combs:
            edges.append(Edge.ByVertices([vertices[comb[0]], vertices[comb[1]]], tolerance=tolerance))
        return Cluster.ByTopologies(edges)
    
    @staticmethod
    def Shells(cluster) -> list:
        """
        Returns the shells of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of shells.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Shells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        shells = []
        _ = cluster.Shells(None, shells)
        return shells

    @staticmethod
    def Simplify(cluster):
        """
        Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        topologic_core.Topology or list
            The simplification of the cluster.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Simplify - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        resultingTopologies = []
        topCC = []
        _ = cluster.CellComplexes(None, topCC)
        topCells = []
        _ = cluster.Cells(None, topCells)
        topShells = []
        _ = cluster.Shells(None, topShells)
        topFaces = []
        _ = cluster.Faces(None, topFaces)
        topWires = []
        _ = cluster.Wires(None, topWires)
        topEdges = []
        _ = cluster.Edges(None, topEdges)
        topVertices = []
        _ = cluster.Vertices(None, topVertices)
        if len(topCC) == 1:
            cc = topCC[0]
            ccVertices = []
            _ = cc.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(cc)
        if len(topCC) == 0 and len(topCells) == 1:
            cell = topCells[0]
            ccVertices = []
            _ = cell.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(cell)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 1:
            shell = topShells[0]
            ccVertices = []
            _ = shell.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(shell)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 1:
            face = topFaces[0]
            ccVertices = []
            _ = face.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(face)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 1:
            wire = topWires[0]
            ccVertices = []
            _ = wire.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(wire)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 1:
            edge = topEdges[0]
            ccVertices = []
            _ = wire.Vertices(None, ccVertices)
            if len(topVertices) == len(ccVertices):
                resultingTopologies.append(edge)
        if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 0 and len(topVertices) == 1:
            vertex = topVertices[0]
            resultingTopologies.append(vertex)
        if len(resultingTopologies) == 1:
            return resultingTopologies[0]
        return cluster

    @staticmethod
    def Vertices(cluster) -> list:
        """
        Returns the vertices of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of vertices.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Vertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        vertices = []
        _ = cluster.Vertices(None, vertices)
        return vertices

    @staticmethod
    def Wires(cluster) -> list:
        """
        Returns the wires of the input cluster.

        Parameters
        ----------
        cluster : topologic_core.Cluster
            The input cluster.

        Returns
        -------
        list
            The list of wires.

        """
        from topologicpy.Topology import Topology

        if not Topology.IsInstance(cluster, "Cluster"):
            print("Cluster.Wires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
            return None
        wires = []
        _ = cluster.Wires(None, wires)
        return wires

Static methods

def ByFormula(formula, xRange=None, yRange=None, xString='X', yString='Y')

Creates a cluster of vertices by evaluating the input formula for a range of x values and, optionally, a range of y values.

Parameters

formula : str
A string representing the formula to be evaluated. For 2D formulas (i.e. Z = 0), use either 'X' (uppercase) or 'Y' (uppercase) for the independent variable. For 3D formulas, use 'X' and 'Y' (uppercase) for the independent variables. The Z value will be evaluated. For 3D formulas, both xRange and yRange MUST be specified. You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc. For example, 'X*2 + 2X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
xRange : tuple , optional
A tuple (start, end, step) representing the range of X values for which the formula should be evaluated. For example, to evaluate Y for X values from -5 to 5 with a step of 0.1, you should specify xRange=(-5, 5, 0.1). If the xRange is set to None or not specified: The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y*2 + 2Y - sqrt(Y)') The method will attempt to evaluate X based on the specified yRange. xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
yRange : tuple , optional
A tuple (start, end, step) representing the range of Y values for which the formula should be evaluated. For example, to evaluate X for Y values from -5 to 5 with a step of 0.1, you should specify yRange=(-5,5,0.1). If the yRange is set to None or not specified: The method assumes that the formula uses the xString (e.g. 'X' as in 'X*2 + 2X - sqrt(X)') The method will attempt to evaluate Y based on the specified xRange. xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
xString : str , optional
The string used to represent the X independent variable. The default is 'X' (uppercase).
yString : str , optional
The string used to represent the Y independent variable. The default is 'Y' (uppercase).

Returns

topologic_core.Cluster The created cluster of vertices.

Expand source code
@staticmethod
def ByFormula(formula, xRange=None, yRange=None, xString="X", yString="Y"):
    """
    Creates a cluster of vertices by evaluating the input formula for a range of x values and, optionally, a range of y values.

    Parameters
    ----------
    formula : str
        A string representing the formula to be evaluated.
        For 2D formulas (i.e. Z = 0), use either 'X' (uppercase) or 'Y' (uppercase) for the independent variable.
        For 3D formulas, use 'X' and 'Y' (uppercase) for the independent variables. The Z value will be evaluated.
        For 3D formulas, both xRange and yRange MUST be specified.
        You can use standard math functions like 'sin', 'cos', 'tan', 'sqrt', etc.
        For example, 'X**2 + 2*X - sqrt(X)' or 'cos(abs(X)+abs(Y))'
    xRange : tuple , optional
        A tuple (start, end, step) representing the range of X values for which the formula should be evaluated.
        For example, to evaluate Y for X values from -5 to 5 with a step of 0.1, you should specify xRange=(-5, 5, 0.1).
        If the xRange is set to None or not specified:
            The method assumes that the formula uses the yString (e.g. 'Y' as in 'Y**2 + 2*Y - sqrt(Y)')
            The method will attempt to evaluate X based on the specified yRange.
            xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
    yRange : tuple , optional
        A tuple (start, end, step) representing the range of Y values for which the formula should be evaluated.
        For example, to evaluate X for Y values from -5 to 5 with a step of 0.1, you should specify yRange=(-5,5,0.1).
        If the yRange is set to None or not specified:
            The method assumes that the formula uses the xString (e.g. 'X' as in 'X**2 + 2*X - sqrt(X)')
            The method will attempt to evaluate Y based on the specified xRange.
            xRange and yRange CANNOT be None or unspecified at the same time. One or the other must be specified.
    xString : str , optional
        The string used to represent the X independent variable. The default is 'X' (uppercase).
    yString : str , optional
        The string used to represent the Y independent variable. The default is 'Y' (uppercase).

    Returns:
        topologic_core.Cluster
            The created cluster of vertices.
    """
    from topologicpy.Vertex import Vertex
    import math
    if xRange == None and yRange == None:
        print("Cluster.ByFormula - Error: Both ranges cannot be None at the same time. Returning None.")
        return None
    if xString.islower():
        print("Cluster.ByFormula - Error: the input xString cannot lowercase. Please consider using uppercase (e.g. X). Returning None.")
        return None
    if yString == 'y':
        print("Cluster.ByFormula - Error: the input yString cannot be lowercase. Please consider using uppercase (e.g. Y). Returning None.")
        return None
    
    x_values = []
    y_values = []
    if not xRange == None:
        x_start, x_end, x_step = xRange
        x = x_start
        while x < x_end:
            x_values.append(x)
            x = x + x_step
        x_values.append(x_end)
    
    if not yRange == None:
        y_start, y_end, y_step = yRange
        y = y_start
        while y < y_end:
            y_values.append(y)
            y = y + y_step
        y_values.append(y_end)

    # Evaluate the formula for each x and y value
    x_return = []
    y_return = []
    z_return = []
    if len(x_values) > 0 and len(y_values) > 0: # Both X and Y exist, compute Z.
        for x in x_values:
            for y in y_values:
                x_return.append(x)
                y_return.append(y)
                formula1 = formula.replace(xString, str(x)).replace(yString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
                z_return.append(eval(formula1))
    elif len(x_values) == 0 and len(y_values) > 0: # Only Y exists, compute X, Z is always 0.
        for y in y_values:
            y_return.append(y)
            formula1 = formula.replace(xString, str(y)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
            x_return.append(eval(formula1))
            z_return.append(0)
    else: # Only X exists, compute Y, Z is always 0.
        for x in x_values:
            x_return.append(x)
            formula1 = formula.replace(xString, str(x)).replace('sqrt', 'math.sqrt').replace('sin', 'math.sin').replace('cos', 'math.cos').replace('tan', 'math.tan').replace('radians', 'math.radians').replace('pi', 'math.pi')
            y_return.append(eval(formula1))
            z_return.append(0)
    vertices = []
    for i in range(len(x_return)):
        vertices.append(Vertex.ByCoordinates(x_return[i], y_return[i], z_return[i]))
    return Cluster.ByTopologies(vertices)
def ByTopologies(*args, transferDictionaries: bool = False)

Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.

Parameters

topologies : list
The list of topologies.
transferDictionaries : bool , optional
If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.

Returns

topologic_core.Cluster
The created topologic Cluster.
Expand source code
@staticmethod
def ByTopologies(*args, transferDictionaries: bool = False):
    """
    Creates a topologic Cluster from the input list of topologies. The input can be individual topologies each as an input argument or a list of topologies stored in one input argument.

    Parameters
    ----------
    topologies : list
        The list of topologies.
    transferDictionaries : bool , optional
        If set to True, the dictionaries from the input topologies are merged and transferred to the cluster. Otherwise they are not. The default is False.

    Returns
    -------
    topologic_core.Cluster
        The created topologic Cluster.

    """
    from topologicpy.Dictionary import Dictionary
    from topologicpy.Topology import Topology
    from topologicpy.Helper import Helper

    if len(args) == 0:
        print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
        return None
    if len(args) == 1:
        topologies = args[0]
        if isinstance(topologies, list):
            if len(topologies) == 0:
                print("Cluster.ByTopologies - Error: The input topologies parameter is an empty list. Returning None.")
                return None
            else:
                topologyList = [x for x in topologies if Topology.IsInstance(x, "Topology")]
                if len(topologies) == 0:
                    print("Cluster.ByTopologies - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
                    return None
        else:
            print("Cluster.ByTopologies - Warning: The input topologies parameter contains only one topology. Returning the same topology.")
            return topologies
    else:
        topologyList = Helper.Flatten(list(args))
        topologyList = [x for x in topologyList if Topology.IsInstance(x, "Topology")]
    if len(topologyList) == 0:
        print("Cluster.ByTopologies - Error: The input parameters do not contain any valid topologies. Returning None.")
        return None
    cluster = topologic.Cluster.ByTopologies(topologyList, False) # Hook to Core
    dictionaries = []
    for t in topologyList:
        d = Topology.Dictionary(t)
        keys = Dictionary.Keys(d)
        if isinstance(keys, list):
            if len(keys) > 0:
                dictionaries.append(d)
    if len(dictionaries) > 0:
        if len(dictionaries) > 1:
            d = Dictionary.ByMergedDictionaries(dictionaries)
        else:
            d = dictionaries[0]
            cluster = Topology.SetDictionary(cluster, d)
    return cluster
def CellComplexes(cluster) ‑> list

Returns the cellComplexes of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of cellComplexes.
Expand source code
@staticmethod
def CellComplexes(cluster) -> list:
    """
    Returns the cellComplexes of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of cellComplexes.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.CellComplexes - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    cellComplexes = []
    _ = cluster.CellComplexes(None, cellComplexes)
    return cellComplexes
def Cells(cluster) ‑> list

Returns the cells of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of cells.
Expand source code
@staticmethod
def Cells(cluster) -> list:
    """
    Returns the cells of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of cells.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Cells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    cells = []
    _ = cluster.Cells(None, cells)
    return cells
def DBSCAN(topologies, selectors=None, keys=['x', 'y', 'z'], epsilon: float = 0.5, minSamples: int = 2)

Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN

Parameters

topologies : list
The input list of topologies to be clustered.
selectors : list , optional
If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies. If set to None, the list of topologies is expected to be a list of vertices. The default is None.
keys : list, optional
The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included, make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
epsilon : float , optional
The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5.
minSamples : int , optional
The minimum number of points required to form a dense region (cluster). The default is 2.

Returns

list, list
The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).
Expand source code
@staticmethod
def DBSCAN(topologies, selectors=None, keys=["x", "y", "z"], epsilon: float = 0.5, minSamples: int = 2):
    """
    Clusters the input vertices based on the Density-Based Spatial Clustering of Applications with Noise (DBSCAN) method. See https://en.wikipedia.org/wiki/DBSCAN

    Parameters
    ----------
    topologies : list
        The input list of topologies to be clustered.
    selectors : list , optional
        If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
        If set to None, the list of topologies is expected to be a list of vertices. The default is None.
    keys : list, optional
        The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
        make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
    epsilon : float , optional
        The maximum radius around a data point within which other points are considered to be part of the same sense region (cluster). The default is 0.5. 
    minSamples : int , optional
        The minimum number of points required to form a dense region (cluster). The default is 2.

    Returns
    -------
    list, list
        The list of clusters and the list of vertices considered to be noise if any (otherwise returns None).

    """
    from topologicpy.Vertex import Vertex
    from topologicpy.Topology import Topology
    from topologicpy.Dictionary import Dictionary

    def dbscan_3d_indices(data, eps, min_samples):
        """
        DBSCAN clustering algorithm for 3D points.

        Parameters:
        - data: NumPy array, input data points with X, Y, and Z coordinates.
        - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
        - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.

        Returns:
        - clusters: List of lists, each list containing the indices of points in a cluster.
        - noise: List of indices, indices of points labeled as noise.
        """

        # Compute pairwise distances
        dists = squareform(pdist(data))

        # Initialize labels and cluster ID
        labels = np.full(data.shape[0], -1)
        cluster_id = 0

        # Iterate through each point
        for i in range(data.shape[0]):
            if labels[i] != -1:
                continue  # Skip already processed points

            # Find neighbors within epsilon distance
            neighbors = np.where(dists[i] < eps)[0]

            if len(neighbors) < min_samples:
                # Label as noise
                labels[i] = -1
            else:
                # Expand cluster
                cluster_id += 1
                expand_cluster_3d_indices(labels, dists, i, neighbors, cluster_id, eps, min_samples)

        # Organize indices into clusters and noise
        clusters = [list(np.where(labels == cid)[0]) for cid in range(1, cluster_id + 1)]
        noise = list(np.where(labels == -1)[0])

        return clusters, noise

    def expand_cluster_3d_indices(labels, dists, point_index, neighbors, cluster_id, eps, min_samples):
        """
        Expand the cluster around a core point for 3D points.

        Parameters:
        - labels: NumPy array, cluster labels for each data point.
        - dists: NumPy array, pairwise distances between data points.
        - point_index: int, index of the core point.
        - neighbors: NumPy array, indices of neighbors.
        - cluster_id: int, current cluster ID.
        - eps: float, maximum distance between two samples for one to be considered as in the neighborhood of the other.
        - min_samples: int, the number of samples (or total weight) in a neighborhood for a point to be considered as a core point.
        """
        labels[point_index] = cluster_id

        i = 0
        while i < len(neighbors):
            current_neighbor = neighbors[i]

            if labels[current_neighbor] == -1:
                labels[current_neighbor] = cluster_id

                new_neighbors = np.where(dists[current_neighbor] < eps)[0]
                if len(new_neighbors) >= min_samples:
                    neighbors = np.concatenate([neighbors, new_neighbors])

            elif labels[current_neighbor] == 0:
                labels[current_neighbor] = cluster_id

            i += 1
    
    if not isinstance(topologies, list):
        print("Cluster.DBSCAN - Error: The input vertices parameter is not a valid list. Returning None.")
        return None, None
    topologyList = [t for t in topologies if Topology.IsInstance(t, "Topology")]
    if len(topologyList) < 1:
        print("Cluster.DBSCAN - Error: The input vertices parameter does not contain any valid vertices. Returning None.")
        return None, None
    if len(topologyList) < minSamples:
        print("Cluster.DBSCAN - Error: The input minSamples parameter cannot be larger than the number of vertices. Returning None.")
        return None, None
    
    if not isinstance(selectors, list):
        check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
        if len(check_vertices) > 0:
            print("Cluster.DBSCAN - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
            return None, None
    else:
        selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
        if len(selectors) < 1:
            check_vertices = [t for t in topologyList if not Topology.IsInstance(t, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.DBSCAN - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None, None
        if not len(selectors) == len(topologyList):
            print("Cluster.DBSCAN - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
            return None, None
    if not isinstance(keys, list):
        print("Cluster.DBSCAN - Error: The input keys parameter is not a valid list. Returning None.")
        return None
    

    data = []
    if selectors == None:
        for t in topologyList:
            elements = []
            if keys:
                d = Topology.Dictionary(t)
                for key in keys:
                    if key.lower() == "x":
                        value = Vertex.X(t)
                    elif key.lower() == "y":
                        value = Vertex.Y(t)
                    elif key.lower() == "z":
                        value = Vertex.Z(t)
                    else:
                        value = Dictionary.ValueAtKey(d, key)
                    if value != None:
                        elements.append(value)
            data.append(elements)
    else:
        for i, s in enumerate(selectors):
            elements = []
            if keys:
                d = Topology.Dictionary(topologyList[i])
                for key in keys:
                    if key.lower() == "x":
                        value = Vertex.X(s)
                    elif key.lower() == "y":
                        value = Vertex.Y(s)
                    elif key.lower() == "z":
                        value = Vertex.Z(s)
                    else:
                        value = Dictionary.ValueAtKey(d, key)
                    if value != None:
                        elements.append(value)
            data.append(elements)
    #coords = [[Vertex.X(v), Vertex.Y(v), Vertex.Z(v)] for v in vertexList]
    clusters, noise = dbscan_3d_indices(np.array(data), epsilon, minSamples)
    tp_clusters = []
    for cluster in clusters:
        tp_clusters.append(Cluster.ByTopologies([topologyList[i] for i in cluster]))
    vert_group = []
    tp_noise = None
    if len(noise) > 0:
        tp_noise = Cluster.ByTopologies([topologyList[i] for i in noise])
    return tp_clusters, tp_noise
def Edges(cluster) ‑> list

Returns the edges of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of edges.
Expand source code
@staticmethod
def Edges(cluster) -> list:
    """
    Returns the edges of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of edges.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Edges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    edges = []
    _ = cluster.Edges(None, edges)
    return edges
def Faces(cluster) ‑> list

Returns the faces of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of faces.
Expand source code
@staticmethod
def Faces(cluster) -> list:
    """
    Returns the faces of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of faces.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Faces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    faces = []
    _ = cluster.Faces(None, faces)
    return faces
def FreeCells(cluster, tolerance: float = 0.0001) ‑> list

Returns the free cells of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free cells.
Expand source code
@staticmethod
def FreeCells(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free cells of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free cells.

    """
    from topologicpy.CellComplex import CellComplex
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeCells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allCells = []
    _ = cluster.Cells(None, allCells)
    if len(allCells) < 1:
        return []
    allCellsCluster = Cluster.ByTopologies(allCells)
    freeCells = []
    cellComplexes = []
    _ = cluster.CellComplexes(None, cellComplexes)
    cellComplexesCells = []
    for cellComplex in cellComplexes:
        tempCells = CellComplex.Cells(cellComplex)
        cellComplexesCells += tempCells
    if len(cellComplexesCells) == 0:
        return allCells
    cellComplexesCluster = Cluster.ByTopologies(cellComplexesCells)
    resultingCluster = Topology.Boolean(allCellsCluster, cellComplexesCluster, operation="difference", tolerance=tolerance)
    if resultingCluster == None:
        return []
    if Topology.IsInstance(resultingCluster, "Cell"):
        return [resultingCluster]
    result = Topology.SubTopologies(resultingCluster, subTopologyType="cell")
    if result == None:
        return [] #Make sure you return an empty list instead of None
    return result
def FreeEdges(cluster, tolerance: float = 0.0001) ‑> list

Returns the free edges of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float, optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free edges.
Expand source code
@staticmethod
def FreeEdges(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free edges of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float, optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free edges.

    """
    from topologicpy.Wire import Wire
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeEdges - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allEdges = []
    _ = cluster.Edges(None, allEdges)
    if len(allEdges) < 1:
        return []
    allEdgesCluster = Cluster.ByTopologies(allEdges)
    wires = []
    _ = cluster.Wires(None, wires)
    wireEdges = []
    for wire in wires:
        tempEdges = Wire.Edges(wire)
        wireEdges += tempEdges
    if len(wireEdges) == 0:
        return allEdges
    wireCluster = Cluster.ByTopologies(wireEdges)
    resultingCluster = Topology.Boolean(allEdgesCluster, wireCluster, operation="difference", tolerance=tolerance)
    if resultingCluster == None:
        return []
    if Topology.IsInstance(resultingCluster, "Edge"):
        return [resultingCluster]
    result = Topology.SubTopologies(resultingCluster, subTopologyType="edge")
    if result == None:
        return [] #Make sure you return an empty list instead of None
    return result
def FreeFaces(cluster, tolerance: float = 0.0001) ‑> list

Returns the free faces of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free faces.
Expand source code
@staticmethod
def FreeFaces(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free faces of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free faces.

    """
    from topologicpy.Shell import Shell
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeFaces - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allFaces = []
    _ = cluster.Faces(None, allFaces)
    if len(allFaces) < 1:
        return []
    allFacesCluster = Cluster.ByTopologies(allFaces)
    shells = []
    _ = cluster.Shells(None, shells)
    shellFaces = []
    for shell in shells:
        tempFaces = Shell.Faces(shell)
        shellFaces += tempFaces
    if len(shellFaces) == 0:
        return allFaces
    shellCluster = Cluster.ByTopologies(shellFaces)
    resultingCluster = Topology.Boolean(allFacesCluster, shellCluster, operation="difference", tolerance=tolerance)
    if resultingCluster == None:
        return []
    if Topology.IsInstance(resultingCluster, "Face"):
        return [resultingCluster]
    result = Topology.SubTopologies(resultingCluster, subTopologyType="face")
    if result == None:
        return [] #Make sure you return an empty list instead of None
    return result
def FreeShells(cluster, tolerance: float = 0.0001) ‑> list

Returns the free shells of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float, optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free shells.
Expand source code
@staticmethod
def FreeShells(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free shells of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float, optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free shells.

    """
    from topologicpy.Cell import Cell
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeShells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allShells = []
    _ = cluster.Shells(None, allShells)
    if len(allShells) < 1:
        return []
    allShellsCluster = Cluster.ByTopologies(allShells)
    cells = []
    _ = cluster.Cells(None, cells)
    cellsShells = []
    for cell in cells:
        tempShells = Cell.Shells(cell)
        cellsShells += tempShells
    if len(cellsShells) == 0:
        return allShells
    cellsCluster = Cluster.ByTopologies(cellsShells)
    resultingCluster = Topology.Boolean(allShellsCluster, cellsCluster, operation="difference", tolerance=tolerance)
    if resultingCluster == None:
        return []
    if Topology.IsInstance(resultingCluster, "Shell"):
        return [resultingCluster]
    result = Topology.SubTopologies(resultingCluster, subTopologyType="shell")
    if result == None:
        return [] #Make sure you return an empty list instead of None
    return result
def FreeTopologies(cluster, tolerance: float = 0.0001) ‑> list

Returns the free topologies of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free topologies.
Expand source code
@staticmethod
def FreeTopologies(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free topologies of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free topologies.

    """
    topologies = Cluster.FreeVertices(cluster, tolerance=tolerance)
    topologies += Cluster.FreeEdges(cluster, tolerance=tolerance)
    topologies += Cluster.FreeWires(cluster, tolerance=tolerance)
    topologies += Cluster.FreeFaces(cluster, tolerance=tolerance)
    topologies += Cluster.FreeShells(cluster, tolerance=tolerance)
    topologies += Cluster.FreeCells(cluster, tolerance=tolerance)
    topologies += Cluster.CellComplexes(cluster)

    return topologies
def FreeVertices(cluster, tolerance: float = 0.0001) ‑> list

Returns the free vertices of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free vertices.
Expand source code
@staticmethod
def FreeVertices(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free vertices of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free vertices.

    """
    from topologicpy.Edge import Edge
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeVertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allVertices = []
    _ = cluster.Vertices(None, allVertices)
    if len(allVertices) < 1:
        return []
    allVerticesCluster = Cluster.ByTopologies(allVertices)
    edges = []
    _ = cluster.Edges(None, edges)
    edgesVertices = []
    for edge in edges:
        tempVertices = Edge.Vertices(edge)
        edgesVertices += tempVertices
    if len(edgesVertices) == 0:
        return allVertices
    edgesCluster = Cluster.ByTopologies(edgesVertices)
    resultingCluster = Topology.Boolean(allVerticesCluster, edgesCluster, operation="difference", tolerance=tolerance)
    if Topology.IsInstance(resultingCluster, "Vertex"):
        return [resultingCluster]
    if resultingCluster == None:
        return []
    result = Topology.SubTopologies(resultingCluster, subTopologyType="vertex")
    if result == None:
        return [] #Make sure you return an empty list instead of None
    return result
def FreeWires(cluster, tolerance: float = 0.0001) ‑> list

Returns the free wires of the input cluster that are not part of a higher topology.

Parameters

cluster : topologic_core.Cluster
The input cluster.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

list
The list of free wires.
Expand source code
@staticmethod
def FreeWires(cluster, tolerance: float = 0.0001) -> list:
    """
    Returns the free wires of the input cluster that are not part of a higher topology.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    list
        The list of free wires.

    """
    from topologicpy.Face import Face
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.FreeWires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    allWires = []
    _ = cluster.Wires(None, allWires)
    if len(allWires) < 1:
        return []
    allWiresCluster = Cluster.ByTopologies(allWires)
    faces = []
    _ = cluster.Faces(None, faces)
    facesWires = []
    for face in faces:
        tempWires = Face.Wires(face)
        facesWires += tempWires
    if len(facesWires) == 0:
        return allWires
    facesCluster = Cluster.ByTopologies(facesWires)
    resultingCluster = Topology.Boolean(allWiresCluster, facesCluster, operation="difference", tolerance=tolerance)
    if resultingCluster == None:
        return []
    if Topology.IsInstance(resultingCluster, "Wire"):
        return [resultingCluster]
    result = Topology.SubTopologies(resultingCluster, subTopologyType="wire")
    if not result:
        return [] #Make sure you return an empty list instead of None
    return result
def HighestType(cluster) ‑> int

Returns the type of the highest dimension subtopology found in the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

int
The type of the highest dimension subtopology found in the input cluster.
Expand source code
@staticmethod
def HighestType(cluster) -> int:
    """
    Returns the type of the highest dimension subtopology found in the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    int
        The type of the highest dimension subtopology found in the input cluster.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.HighestType - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    cellComplexes = Cluster.CellComplexes(cluster)
    if len(cellComplexes) > 0:
        return Topology.TypeID("CellComplex")
    cells = Cluster.Cells(cluster)
    if len(cells) > 0:
        return Topology.TypeID("Cell")
    shells = Cluster.Shells(cluster)
    if len(shells) > 0:
        return Topology.TypeID("Shell")
    faces = Cluster.Faces(cluster)
    if len(faces) > 0:
        return Topology.TypeID("Face")
    wires = Cluster.Wires(cluster)
    if len(wires) > 0:
        return Topology.TypeID("Wire")
    edges = Cluster.Edges(cluster)
    if len(edges) > 0:
        return Topology.TypeID("Edge")
    vertices = Cluster.Vertices(cluster)
    if len(vertices) > 0:
        return Topology.TypeID("Vertex")
def K_Means(topologies, selectors=None, keys=['x', 'y', 'z'], k=4, maxIterations=100, centroidKey='k_centroid')

Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering

Parameters

topologies : list
The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
selectors : list , optional
If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies. If set to None, the list of topologies is expected to be a list of vertices. The default is None.
keys : list, optional
The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included, make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
k : int , optional
The desired number of clusters. The default is 4.
maxIterations : int , optional
The desired maximum number of iterations for the clustering algorithm
centroidKey : str , optional
The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"

Returns

list
The created list of clusters.
Expand source code
@staticmethod
def K_Means(topologies, selectors=None, keys=["x", "y", "z"], k=4, maxIterations=100, centroidKey="k_centroid"):
    """
    Clusters the input topologies using K-Means clustering. See https://en.wikipedia.org/wiki/K-means_clustering

    Parameters
    ----------
    topologies : list
        The input list of topologies. If this is not a list of topologic vertices then please provide a list of selectors
    selectors : list , optional
        If the list of topologies are not vertices then please provide a corresponding list of selectors (vertices) that represent the topologies for clustering. For example, these can be the centroids of the topologies.
        If set to None, the list of topologies is expected to be a list of vertices. The default is None.
    keys : list, optional
        The keys in the embedded dictionaries in the topologies. If specified, the values at these keys will be added to the dimensions to be clustered. The values must be numeric. If you wish the x, y, z location to be included,
        make sure the keys list includes "X", "Y", and/or "Z" (case insensitive). The default is ["x", "y", "z"]
    k : int , optional
        The desired number of clusters. The default is 4.
    maxIterations : int , optional
        The desired maximum number of iterations for the clustering algorithm
    centroidKey : str , optional
        The desired dictionary key under which to store the cluster's centroid (this is not to be confused with the actual geometric centroid of the cluster). The default is "k_centroid"

    Returns
    -------
    list
        The created list of clusters.

    """
    from topologicpy.Helper import Helper
    from topologicpy.Vertex import Vertex
    from topologicpy.Dictionary import Dictionary
    from topologicpy.Topology import Topology


    def k_means(data, vertices, k=4, maxIterations=100):
        import random
        def euclidean_distance(p, q):
            return sum((pi - qi) ** 2 for pi, qi in zip(p, q)) ** 0.5

        # Initialize k centroids randomly
        centroids = random.sample(data, k)

        for _ in range(maxIterations):
            # Assign each data point to the nearest centroid
            clusters = [[] for _ in range(k)]
            clusters_v = [[] for _ in range(k)]
            for i, point in enumerate(data):
                distances = [euclidean_distance(point, centroid) for centroid in centroids]
                nearest_centroid_index = distances.index(min(distances))
                clusters[nearest_centroid_index].append(point)
                clusters_v[nearest_centroid_index].append(vertices[i])

            # Compute the new centroids as the mean of the points in each cluster
            new_centroids = []
            for cluster in clusters:
                if not cluster:
                    # If a cluster is empty, keep the previous centroid
                    new_centroids.append(centroids[clusters.index(cluster)])
                else:
                    new_centroids.append([sum(dim) / len(cluster) for dim in zip(*cluster)])

            # Check if the centroids have converged
            if new_centroids == centroids:
                break

            centroids = new_centroids

        return {'clusters': clusters, 'clusters_v': clusters_v, 'centroids': centroids}



    if not isinstance(topologies, list):
        print("Cluster.K_Means - Error: The input topologies parameter is not a valid list. Returning None.")
        return None
    topologies = [t for t in topologies if Topology.IsInstance(t, "Topology")]
    if len(topologies) < 1:
        print("Cluster.K_Means - Error: The input topologies parameter does not contain any valid topologies. Returning None.")
        return None
    if not isinstance(selectors, list):
        check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
        if len(check_vertices) > 0:
            print("Cluster.K_Means - Error: The input selectors parameter is not a valid list and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
            return None
    else:
        selectors = [s for s in selectors if Topology.IsInstance(s, "Vertex")]
        if len(selectors) < 1:
            check_vertices = [v for v in topologies if not Topology.IsInstance(v, "Vertex")]
            if len(check_vertices) > 0:
                print("Cluster.K_Means - Error: The input selectors parameter does not contain any valid vertices and this is needed since the list of topologies contains objects of type other than a topologic_core.Vertex. Returning None.")
                return None
        if not len(selectors) == len(topologies):
            print("Cluster.K_Means - Error: The input topologies and selectors parameters do not have the same length. Returning None.")
            return None
    if not isinstance(keys, list):
        print("Cluster.K_Means - Error: The input keys parameter is not a valid list. Returning None.")
        return None
    if not isinstance(k , int):
        print("Cluster.K_Means - Error: The input k parameter is not a valid integer. Returning None.")
        return None
    if k < 1:
        print("Cluster.K_Means - Error: The input k parameter is less than one. Returning None.")
        return None
    if len(topologies) < k:
        print("Cluster.K_Means - Error: The input topologies parameter is less than the specified number of clusters. Returning None.")
        return None
    if len(topologies) == k:
        t_clusters = []
        for topology in topologies:
            t_cluster = Cluster.ByTopologies([topology])
            for key in keys:
                    if key.lower() == "x":
                        value = Vertex.X(t)
                    elif key.lower() == "y":
                        value = Vertex.Y(t)
                    elif key.lower() == "z":
                        value = Vertex.Z(t)
                    else:
                        value = Dictionary.ValueAtKey(d, key)
                    if value != None:
                        elements.append(value)
            d = Dictionary.ByKeysValues([centroidKey], [elements])
            t_cluster = Topology.SetDictionary(t_cluster, d)
            t_clusters.append(t_cluster)
        return t_clusters
    
    data = []
    if selectors == None:
        for t in topologies:
            elements = []
            if keys:
                d = Topology.Dictionary(t)
                for key in keys:
                    if key.lower() == "x":
                        value = Vertex.X(t)
                    elif key.lower() == "y":
                        value = Vertex.Y(t)
                    elif key.lower() == "z":
                        value = Vertex.Z(t)
                    else:
                        value = Dictionary.ValueAtKey(d, key)
                    if value != None:
                        elements.append(value)
            data.append(elements)
    else:
        for i, s in enumerate(selectors):
            elements = []
            if keys:
                d = Topology.Dictionary(topologies[i])
                for key in keys:
                    if key.lower() == "x":
                        value = Vertex.X(s)
                    elif key.lower() == "y":
                        value = Vertex.Y(s)
                    elif key.lower() == "z":
                        value = Vertex.Z(s)
                    else:
                        value = Dictionary.ValueAtKey(d, key)
                    if value != None:
                        elements.append(value)
            data.append(elements)
    if len(data) == 0:
        print("Cluster.K_Means - Error: Could not perform the operation. Returning None.")
        return None
    if selectors:
        dict = k_means(data, selectors, k=k, maxIterations=maxIterations)
    else:
        dict = k_means(data, topologies, k=k, maxIterations=maxIterations)
    clusters = dict['clusters_v']
    centroids = dict['centroids']
    t_clusters = []
    for i, cluster in enumerate(clusters):
        cluster_vertices = []
        for v in cluster:
            if selectors == None:
                cluster_vertices.append(v)
            else:
                index = selectors.index(v)
                cluster_vertices.append(topologies[index])
        cluster = Cluster.ByTopologies(cluster_vertices)
        d = Dictionary.ByKeysValues([centroidKey], [centroids[i]])
        cluster = Topology.SetDictionary(cluster, d)
        t_clusters.append(cluster)
    return t_clusters
def MergeCells(cells, tolerance=0.0001)

Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.

Parameters

cells : list
The input list of cells.
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

topologic_core.Cluster
The created cluster with merged cells as possible.
Expand source code
@staticmethod
def MergeCells(cells, tolerance=0.0001):
    """
    Creates a cluster that contains cellComplexes where it can create them plus any additional free cells.

    Parameters
    ----------
    cells : list
        The input list of cells.
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    topologic_core.Cluster
        The created cluster with merged cells as possible.

    """

    from topologicpy.CellComplex import CellComplex
    from topologicpy.Topology import Topology

    def find_cell_complexes(cells, adjacency_test, tolerance=0.0001):
        cell_complexes = []
        remaining_cells = set(cells)

        def explore_complex(cell_complex, remaining, tolerance=0.0001):
            new_cells = set()
            for cell in remaining:
                if any(adjacency_test(cell, existing_cell, tolerance=tolerance) for existing_cell in cell_complex):
                    new_cells.add(cell)
            return new_cells

        while remaining_cells:
            current_cell = remaining_cells.pop()
            current_complex = {current_cell}
            current_complex.update(explore_complex(current_complex, remaining_cells, tolerance=tolerance))
            cell_complexes.append(current_complex)
            remaining_cells -= current_complex

        return cell_complexes

    # Example adjacency test function (replace this with your actual implementation)
    def adjacency_test(cell1, cell2, tolerance=0.0001):
        return Topology.IsInstance(Topology.Merge(cell1, cell2, tolerance=tolerance), "CellComplex")

    if not isinstance(cells, list):
        print("Cluster.MergeCells - Error: The input cells parameter is not a valid list of cells. Returning None.")
        return None
    #cells = [cell for cell in cells if Topology.IsInstance(cell, "Cell")]
    if len(cells) < 1:
        print("Cluster.MergeCells - Error: The input cells parameter does not contain any valid cells. Returning None.")
        return None
    
    complexes = find_cell_complexes(cells, adjacency_test)
    cellComplexes = []
    cells = []
    for aComplex in complexes:
        aComplex = list(aComplex)
        if len(aComplex) > 1:
            cc = CellComplex.ByCells(aComplex, silent=True)
            if Topology.IsInstance(cc, "CellComplex"):
                cellComplexes.append(cc)
        elif len(aComplex) == 1:
            if Topology.IsInstance(aComplex[0], "Cell"):
                cells.append(aComplex[0])
    return Cluster.ByTopologies(cellComplexes+cells)
def MysticRose(wire=None, origin=None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement: str = 'center', tolerance: float = 0.0001)

Creates a mystic rose.

Parameters

wire : topologic_core.Wire , optional
The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
origin : topologic_core.Vertex , optional
The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
radius : float , optional
The radius of the mystic rose. The default is 1.
sides : int , optional
The number of sides of the mystic rose. The default is 16.
perimeter : bool , optional
If True, the perimeter edges are included in the output. The default is True.
direction : list , optional
The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
placement : str , optional
The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
tolerance : float , optional
The desired tolerance. The default is 0.0001.

Returns

topologic_core.Cluster
The created mystic rose (cluster of edges).
Expand source code
@staticmethod
def MysticRose(wire= None, origin= None, radius: float = 0.5, sides: int = 16, perimeter: bool = True, direction: list = [0, 0, 1], placement:str = "center", tolerance: float = 0.0001):
    """
    Creates a mystic rose.

    Parameters
    ----------
    wire : topologic_core.Wire , optional
        The input Wire. if set to None, a circle with the input parameters is created. Otherwise, the input parameters are ignored.
    origin : topologic_core.Vertex , optional
        The location of the origin of the circle. The default is None which results in the circle being placed at (0, 0, 0).
    radius : float , optional
        The radius of the mystic rose. The default is 1.
    sides : int , optional
        The number of sides of the mystic rose. The default is 16.
    perimeter : bool , optional
        If True, the perimeter edges are included in the output. The default is True.
    direction : list , optional
        The vector representing the up direction of the mystic rose. The default is [0, 0, 1].
    placement : str , optional
        The description of the placement of the origin of the mystic rose. This can be "center", or "lowerleft". It is case insensitive. The default is "center".
    tolerance : float , optional
        The desired tolerance. The default is 0.0001.

    Returns
    -------
    topologic_core.Cluster
        The created mystic rose (cluster of edges).

    """
    import topologicpy
    from topologicpy.Vertex import Vertex
    from topologicpy.Edge import Edge
    from topologicpy.Wire import Wire
    from topologicpy.Cluster import Cluster
    from itertools import combinations

    if wire == None:
        wire = Wire.Circle(origin=origin, radius=radius, sides=sides, fromAngle=0, toAngle=360, close=True, direction=direction, placement=placement, tolerance=tolerance)
    if not Wire.IsClosed(wire):
        print("Cluster.MysticRose - Error: The input wire parameter is not a closed topologic wire. Returning None.")
        return None
    vertices = Wire.Vertices(wire)
    indices = list(range(len(vertices)))
    combs = [[comb[0],comb[1]] for comb in combinations(indices, 2) if not (abs(comb[0]-comb[1]) == 1) and not (abs(comb[0]-comb[1]) == len(indices)-1)]
    edges = []
    if perimeter:
        edges = Wire.Edges(wire)
    for comb in combs:
        edges.append(Edge.ByVertices([vertices[comb[0]], vertices[comb[1]]], tolerance=tolerance))
    return Cluster.ByTopologies(edges)
def Shells(cluster) ‑> list

Returns the shells of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of shells.
Expand source code
@staticmethod
def Shells(cluster) -> list:
    """
    Returns the shells of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of shells.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Shells - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    shells = []
    _ = cluster.Shells(None, shells)
    return shells
def Simplify(cluster)

Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

topologic_core.Topology or list
The simplification of the cluster.
Expand source code
@staticmethod
def Simplify(cluster):
    """
    Simplifies the input cluster if possible. For example, if the cluster contains only one cell, that cell is returned.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    topologic_core.Topology or list
        The simplification of the cluster.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Simplify - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    resultingTopologies = []
    topCC = []
    _ = cluster.CellComplexes(None, topCC)
    topCells = []
    _ = cluster.Cells(None, topCells)
    topShells = []
    _ = cluster.Shells(None, topShells)
    topFaces = []
    _ = cluster.Faces(None, topFaces)
    topWires = []
    _ = cluster.Wires(None, topWires)
    topEdges = []
    _ = cluster.Edges(None, topEdges)
    topVertices = []
    _ = cluster.Vertices(None, topVertices)
    if len(topCC) == 1:
        cc = topCC[0]
        ccVertices = []
        _ = cc.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(cc)
    if len(topCC) == 0 and len(topCells) == 1:
        cell = topCells[0]
        ccVertices = []
        _ = cell.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(cell)
    if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 1:
        shell = topShells[0]
        ccVertices = []
        _ = shell.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(shell)
    if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 1:
        face = topFaces[0]
        ccVertices = []
        _ = face.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(face)
    if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 1:
        wire = topWires[0]
        ccVertices = []
        _ = wire.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(wire)
    if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 1:
        edge = topEdges[0]
        ccVertices = []
        _ = wire.Vertices(None, ccVertices)
        if len(topVertices) == len(ccVertices):
            resultingTopologies.append(edge)
    if len(topCC) == 0 and len(topCells) == 0 and len(topShells) == 0 and len(topFaces) == 0 and len(topWires) == 0 and len(topEdges) == 0 and len(topVertices) == 1:
        vertex = topVertices[0]
        resultingTopologies.append(vertex)
    if len(resultingTopologies) == 1:
        return resultingTopologies[0]
    return cluster
def Vertices(cluster) ‑> list

Returns the vertices of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of vertices.
Expand source code
@staticmethod
def Vertices(cluster) -> list:
    """
    Returns the vertices of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of vertices.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Vertices - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    vertices = []
    _ = cluster.Vertices(None, vertices)
    return vertices
def Wires(cluster) ‑> list

Returns the wires of the input cluster.

Parameters

cluster : topologic_core.Cluster
The input cluster.

Returns

list
The list of wires.
Expand source code
@staticmethod
def Wires(cluster) -> list:
    """
    Returns the wires of the input cluster.

    Parameters
    ----------
    cluster : topologic_core.Cluster
        The input cluster.

    Returns
    -------
    list
        The list of wires.

    """
    from topologicpy.Topology import Topology

    if not Topology.IsInstance(cluster, "Cluster"):
        print("Cluster.Wires - Error: The input cluster parameter is not a valid topologic cluster. Returning None.")
        return None
    wires = []
    _ = cluster.Wires(None, wires)
    return wires