K-Means++ Clustering – Files

Custom Python component for Grasshopper which gives a lot of freedom to tweak the parameters and customization of the standard algorithm, such as for example comparative studies of different feature vector set-ups.

Our OpenAccess eCAADe 2025 Paper “Condensing Complexity: K-means clustering of geometric data sets as a design tool” – documenting our approach and usage – can be found on:

ResearchGate:
https://www.researchgate.net/publication/401344242_Condensing_Complexity_K-means_clustering_of_geometric_data_sets_as_a_design_tool

CumInCAD:
https://papers.cumincad.org/cgi-bin/works/paper/ecaade2025_521

Python 3 code component for Rhino 8+Grasshopper:

Python 3 code for Rhino 8+Grasshopper code as a .py file:

Python 3 for Rhino 8+Grasshopper code as plain text:

import math
import random as rnd
import ghpythonlib.treehelpers as th

# Function to remap a value from one range to another
def translate(value, leftMin, leftMax, rightMin, rightMax):
    leftSpan = leftMax - leftMin
    rightSpan = rightMax - rightMin
    valueScaled = (value - leftMin) / leftSpan
    return rightMin + (valueScaled * rightSpan)

# Function to normalize values to a target range [0, 1]
def normalize_values(lst, min_val, max_val):
    return [
        [translate(value, min_val, max_val, 0, 1) for value in sublist] 
        for sublist in lst
    ]

# Euclidean distance function for two vectors
def vector_dist(v1, v2):
    return math.sqrt(sum((v2[i] - v1[i])**2 for i in range(len(v1))))

# KMeans++ initialization: Select initial cluster centers
def kMeansPlusPlusInitialization(vals, cls_num, first_center_index):
    if not ini:
        rnd.seed(42)  # Set fixed seed for deterministic results
        
    cluster_centers = [] 
    first_center = vals[first_center_index]  # Choose first cluster center manually via component input
    cluster_centers.append(first_center)
    
    for _ in range(1, cls_num):
        distances = [
            min(vector_dist(val, center) for center in cluster_centers) 
            for val in vals
        ]
        total_distance = sum(distances)
        probabilities = [dist / total_distance for dist in distances]

        cumulative_probabilities = []
        cumulative_sum = 0
        for p in probabilities:
            cumulative_sum += p
            cumulative_probabilities.append(cumulative_sum)
        
        r = rnd.random()
        for i, cumulative_prob in enumerate(cumulative_probabilities):
            if r < cumulative_prob:
                cluster_centers.append(vals[i])
                break
    
    return cluster_centers

# Main K-Means clustering algorithm
def kMeanClustering(vals, pt_geometry, cls_num, iterations, first_center_index):
    # Normalize the input values
    norm_list = normalize_values(vals, min_val, max_val)
    
    # Initialize cluster centers using KMeans++
    cluster_centers = kMeansPlusPlusInitialization(norm_list, cls_num, first_center_index)
    
    for _ in range(iterations):
        clusters = [[] for _ in cluster_centers]
        clusters_ids = [[] for _ in cluster_centers]
        geom_clusters = [[] for _ in cluster_centers]
        
        for i, norm_val in enumerate(norm_list):
            # Find the closest cluster center for each value
            distances = [vector_dist(norm_val, center) for center in cluster_centers]
            best_center_id = distances.index(min(distances))
            
            clusters[best_center_id].append(norm_val)
            clusters_ids[best_center_id].append(i)
            geom_clusters[best_center_id].append(pt_geometry[i])
        
        # Update cluster centers as the mean of assigned points
        for i, cluster in enumerate(clusters):
            if cluster:
                new_center = [sum(dim) / len(cluster) for dim in zip(*cluster)]
                cluster_centers[i] = new_center
            else:
                cluster_centers[i] = rnd.choice(norm_list)
    
    # Compute geometric averages
    avg_geometry = []
    for cluster in geom_clusters:
        if cluster:
            avg_geom = [sum(dim) / len(cluster) for dim in zip(*cluster)]
            avg_geometry.append(avg_geom)
        else:
            avg_geometry.append([])
    
    # Map the cluster centers back to the original domain
    mapped_centers = [
        [translate(center[i], 0, 1, min_val, max_val) for i in range(len(center))]
        for center in cluster_centers
    ]
    
    # Return cluster IDs, mapped cluster centers, and geometric averages as Grasshopper DataTrees
    return th.list_to_tree(clusters_ids), th.list_to_tree(mapped_centers), th.list_to_tree(avg_geometry)

# Main script execution
# Flattened input: Extract values from Grasshopper tree structure
values = [[item for item in branch] for branch in feature_vector.Branches]
pt_geom = [[item for item in branch] for branch in pt_geometry.Branches]

# Compute min_val and max_val for the entire dataset
all_values = [item for sublist in values for item in sublist]
min_val = min(all_values)
max_val = max(all_values)

# Perform clustering
ids, avg_feature_vector, avg_geometry = kMeanClustering(values, pt_geom, clusters, iterations, first_center_index)

This work and all files related to it are shared under the Creative Commons: Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0) Licence.